xMerge branch 'canary' into feature/delete-docker-volumes

This commit is contained in:
djknaeckebrot
2024-12-18 12:10:04 +01:00
73 changed files with 7747 additions and 1071 deletions

View File

@@ -20,6 +20,8 @@ import {
getUserByToken,
lucia,
luciaToken,
removeAdminByAuthId,
removeUserByAuthId,
sendDiscordNotification,
sendEmailNotification,
updateAuthById,
@@ -59,14 +61,20 @@ export const authRouter = createTRPCRouter({
if (IS_CLOUD) {
await sendDiscordNotificationWelcome(newAdmin);
await sendVerificationEmail(newAdmin.id);
return true;
return {
status: "success",
type: "cloud",
};
}
const session = await lucia.createSession(newAdmin.id || "", {});
ctx.res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
return true;
return {
status: "success",
type: "selfhosted",
};
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -178,6 +186,20 @@ export const authRouter = createTRPCRouter({
update: protectedProcedure
.input(apiUpdateAuth)
.mutation(async ({ ctx, input }) => {
const currentAuth = await findAuthByEmail(ctx.user.email);
if (input.password) {
const correctPassword = bcrypt.compareSync(
input.password,
currentAuth?.password || "",
);
if (!correctPassword) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Current password is incorrect",
});
}
}
const auth = await updateAuthById(ctx.user.authId, {
...(input.email && { email: input.email.toLowerCase() }),
...(input.password && {
@@ -188,6 +210,47 @@ export const authRouter = createTRPCRouter({
return auth;
}),
removeSelfAccount: protectedProcedure
.input(
z.object({
password: z.string().min(1),
}),
)
.mutation(async ({ ctx, input }) => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "This feature is only available in the cloud version",
});
}
const currentAuth = await findAuthByEmail(ctx.user.email);
const correctPassword = bcrypt.compareSync(
input.password,
currentAuth?.password || "",
);
if (!correctPassword) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Password is incorrect",
});
}
const { req, res } = ctx;
const { session } = await validateRequest(req, res);
if (!session) return false;
await lucia.invalidateSession(session.id);
res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
if (ctx.user.rol === "admin") {
await removeAdminByAuthId(ctx.user.authId);
} else {
await removeUserByAuthId(ctx.user.authId);
}
return true;
}),
generateToken: protectedProcedure.mutation(async ({ ctx, input }) => {
const auth = await findAuthById(ctx.user.authId);
@@ -440,7 +503,7 @@ export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => {
webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
},
{
title: " New User Registered",
title: " New User Registered",
color: 0x00ff00,
fields: [
{

View File

@@ -18,6 +18,7 @@ import {
import {
IS_CLOUD,
createServer,
defaultCommand,
deleteServer,
findAdminById,
findServerById,
@@ -25,6 +26,7 @@ import {
getPublicIpWithFallback,
haveActiveServices,
removeDeploymentsByServerId,
serverAudit,
serverSetup,
serverValidate,
updateServerById,
@@ -69,6 +71,11 @@ export const serverRouter = createTRPCRouter({
return server;
}),
getDefaultCommand: protectedProcedure
.input(apiFindOneServer)
.query(async ({ input, ctx }) => {
return defaultCommand();
}),
all: protectedProcedure.query(async ({ ctx }) => {
const result = await db
.select({
@@ -160,6 +167,57 @@ export const serverRouter = createTRPCRouter({
});
}
}),
security: protectedProcedure
.input(apiFindOneServer)
.query(async ({ input, ctx }) => {
try {
const server = await findServerById(input.serverId);
if (server.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to validate this server",
});
}
const response = await serverAudit(input.serverId);
return response as unknown as {
ufw: {
installed: boolean;
active: boolean;
defaultIncoming: string;
};
ssh: {
enabled: boolean;
keyAuth: boolean;
permitRootLogin: string;
passwordAuth: string;
usePam: string;
};
nonRootUser: {
hasValidSudoUser: boolean;
};
unattendedUpgrades: {
installed: boolean;
active: boolean;
updateEnabled: number;
upgradeEnabled: number;
};
fail2ban: {
installed: boolean;
enabled: boolean;
active: boolean;
sshEnabled: string;
sshMode: string;
};
};
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error?.message : `Error: ${error}`,
cause: error as Error,
});
}
}),
remove: protectedProcedure
.input(apiRemoveServer)
.mutation(async ({ input, ctx }) => {

View File

@@ -17,7 +17,7 @@ import {
updateSSHKeyById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { desc, eq } from "drizzle-orm";
export const sshRouter = createTRPCRouter({
create: protectedProcedure
@@ -71,6 +71,7 @@ export const sshRouter = createTRPCRouter({
all: protectedProcedure.query(async ({ ctx }) => {
return await db.query.sshKeys.findMany({
...(IS_CLOUD && { where: eq(sshKeys.adminId, ctx.user.adminId) }),
orderBy: desc(sshKeys.createdAt),
});
// TODO: Remove this line when the cloud version is ready
}),

View File

@@ -81,7 +81,7 @@ export const stripeRouter = createTRPCRouter({
adminId: admin.adminId,
},
allow_promotion_codes: true,
success_url: `${WEBSITE_URL}/dashboard/settings/billing`,
success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`,
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
});

View File

@@ -31,6 +31,8 @@ export const setupDockerContainerLogsWebSocketServer = (
const url = new URL(req.url || "", `http://${req.headers.host}`);
const containerId = url.searchParams.get("containerId");
const tail = url.searchParams.get("tail");
const search = url.searchParams.get("search");
const since = url.searchParams.get("since");
const serverId = url.searchParams.get("serverId");
const { user, session } = await validateWebSocketRequest(req);
@@ -51,9 +53,13 @@ export const setupDockerContainerLogsWebSocketServer = (
const client = new Client();
client
.once("ready", () => {
const command = `
bash -c "docker container logs --tail ${tail} --follow ${containerId}"
`;
const baseCommand = `docker container logs --timestamps --tail ${tail} ${
since === "all" ? "" : `--since ${since}`
} --follow ${containerId}`;
const escapedSearch = search ? search.replace(/'/g, "'\\''") : "";
const command = search
? `${baseCommand} 2>&1 | grep --line-buffered -iF "${escapedSearch}"`
: baseCommand;
client.exec(command, (err, stream) => {
if (err) {
console.error("Execution error:", err);
@@ -91,21 +97,20 @@ export const setupDockerContainerLogsWebSocketServer = (
});
} else {
const shell = getShell();
const ptyProcess = spawn(
shell,
[
"-c",
`docker container logs --tail ${tail} --follow ${containerId}`,
],
{
name: "xterm-256color",
cwd: process.env.HOME,
env: process.env,
encoding: "utf8",
cols: 80,
rows: 30,
},
);
const baseCommand = `docker container logs --timestamps --tail ${tail} ${
since === "all" ? "" : `--since ${since}`
} --follow ${containerId}`;
const command = search
? `${baseCommand} 2>&1 | grep -iF '${search}'`
: baseCommand;
const ptyProcess = spawn(shell, ["-c", command], {
name: "xterm-256color",
cwd: process.env.HOME,
env: process.env,
encoding: "utf8",
cols: 80,
rows: 30,
});
ptyProcess.onData((data) => {
ws.send(data);

View File

@@ -102,21 +102,13 @@ export const setupDockerContainerTerminalWebSocketServer = (
port: server.port,
username: server.username,
privateKey: server.sshKey?.privateKey,
timeout: 99999,
});
} else {
const shell = getShell();
const ptyProcess = spawn(
shell,
["-c", `docker exec -it ${containerId} ${activeWay}`],
{
name: "xterm-256color",
cwd: process.env.HOME,
env: process.env,
encoding: "utf8",
cols: 80,
rows: 30,
},
{},
);
ptyProcess.onData((data) => {

View File

@@ -70,53 +70,44 @@ export const setupTerminalWebSocketServer = (
let stderr = "";
conn
.once("ready", () => {
conn.shell(
{
term: "terminal",
cols: 80,
rows: 30,
height: 30,
width: 80,
},
(err, stream) => {
if (err) throw err;
conn.shell({}, (err, stream) => {
if (err) throw err;
stream
.on("close", (code: number, signal: string) => {
ws.send(`\nContainer closed with code: ${code}\n`);
conn.end();
})
.on("data", (data: string) => {
stdout += data.toString();
ws.send(data.toString());
})
.stderr.on("data", (data) => {
stderr += data.toString();
ws.send(data.toString());
console.error("Error: ", data.toString());
});
stream
.on("close", (code: number, signal: string) => {
ws.send(`\nContainer closed with code: ${code}\n`);
conn.end();
})
.on("data", (data: string) => {
stdout += data.toString();
ws.send(data.toString());
})
.stderr.on("data", (data) => {
stderr += data.toString();
ws.send(data.toString());
console.error("Error: ", data.toString());
});
ws.on("message", (message) => {
try {
let command: string | Buffer[] | Buffer | ArrayBuffer;
if (Buffer.isBuffer(message)) {
command = message.toString("utf8");
} else {
command = message;
}
stream.write(command.toString());
} catch (error) {
// @ts-ignore
const errorMessage = error?.message as unknown as string;
ws.send(errorMessage);
ws.on("message", (message) => {
try {
let command: string | Buffer[] | Buffer | ArrayBuffer;
if (Buffer.isBuffer(message)) {
command = message.toString("utf8");
} else {
command = message;
}
});
stream.write(command.toString());
} catch (error) {
// @ts-ignore
const errorMessage = error?.message as unknown as string;
ws.send(errorMessage);
}
});
ws.on("close", () => {
stream.end();
});
},
);
ws.on("close", () => {
stream.end();
});
});
})
.on("error", (err) => {
if (err.level === "client-authentication") {
@@ -133,7 +124,6 @@ export const setupTerminalWebSocketServer = (
port: server.port,
username: server.username,
privateKey: server.sshKey?.privateKey,
timeout: 99999,
});
});
};