diff --git a/apps/dokploy/__test__/templates/config.template.test.ts b/apps/dokploy/__test__/templates/config.template.test.ts index 6f5baaf1..202abdf2 100644 --- a/apps/dokploy/__test__/templates/config.template.test.ts +++ b/apps/dokploy/__test__/templates/config.template.test.ts @@ -58,10 +58,10 @@ describe("processTemplate", () => { variables: { jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ", anon_payload: JSON.stringify({ - "role": "tester", - "iss": "dockploy", - "iat": "${timestamps:2025-01-01T00:00:00Z}", - "exp": "${timestamps:2030-01-01T00:00:00Z}", + role: "tester", + iss: "dockploy", + iat: "${timestamps:2025-01-01T00:00:00Z}", + exp: "${timestamps:2030-01-01T00:00:00Z}", }), anon_key: "${jwt:jwt_secret:anon_payload}", }, @@ -74,7 +74,9 @@ describe("processTemplate", () => { }; const result = processTemplate(template, mockSchema); expect(result.envs).toHaveLength(1); - expect(result.envs).toContain("ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY"); + expect(result.envs).toContain( + "ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY", + ); expect(result.mounts).toHaveLength(0); expect(result.domains).toHaveLength(0); }); diff --git a/apps/dokploy/__test__/templates/helpers.template.test.ts b/apps/dokploy/__test__/templates/helpers.template.test.ts index d6eb532c..1144b65f 100644 --- a/apps/dokploy/__test__/templates/helpers.template.test.ts +++ b/apps/dokploy/__test__/templates/helpers.template.test.ts @@ -2,7 +2,6 @@ import type { Schema } from "@dokploy/server/templates"; import { processValue } from "@dokploy/server/templates/processors"; import { describe, expect, it } from "vitest"; - describe("helpers functions", () => { // Mock schema for testing const mockSchema: Schema = { @@ -30,7 +29,11 @@ describe("helpers functions", () => { it("should generate a random domain", () => { const domain = processValue("${domain}", {}, mockSchema); expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy(); - expect(domain.endsWith(`${mockSchema.serverIp.replaceAll(".","-")}.traefik.me`)).toBeTruthy(); + expect( + domain.endsWith( + `${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`, + ), + ).toBeTruthy(); }); }); @@ -46,11 +49,14 @@ describe("helpers functions", () => { [32, 44], [64, 88], [128, 172], - ])("should generate a base64 string from parameter %d bytes length", (length, finalLength) => { - const base64 = processValue(`\${base64:${length}}`, {}, mockSchema); - expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/); - expect(base64.length).toBe(finalLength); - }); + ])( + "should generate a base64 string from parameter %d bytes length", + (length, finalLength) => { + const base64 = processValue(`\${base64:${length}}`, {}, mockSchema); + expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/); + expect(base64.length).toBe(finalLength); + }, + ); }); describe("${password}", () => { @@ -58,11 +64,14 @@ describe("helpers functions", () => { const password = processValue("${password}", {}, mockSchema); expect(password).toMatch(/^[A-Za-z0-9]+$/); }); - it.each([6,8,12,16,32])("should generate a password string respecting parameter %d length", (length) => { - const password = processValue(`\${password:${length}}`, {}, mockSchema); - expect(password).toMatch(/^[A-Za-z0-9]+$/); - expect(password.length).toBe(length); - }); + it.each([6, 8, 12, 16, 32])( + "should generate a password string respecting parameter %d length", + (length) => { + const password = processValue(`\${password:${length}}`, {}, mockSchema); + expect(password).toMatch(/^[A-Za-z0-9]+$/); + expect(password.length).toBe(length); + }, + ); }); describe("${hash}", () => { @@ -70,17 +79,22 @@ describe("helpers functions", () => { const hash = processValue("${hash}", {}, mockSchema); expect(hash).toMatch(/^[A-Za-z0-9]+$/); }); - it.each([6,8,12,16,32])("should generate a hash string respecting parameter %d length", (length) => { - const hash = processValue(`\${hash:${length}}`, {}, mockSchema); - expect(hash).toMatch(/^[A-Za-z0-9]+$/); - expect(hash.length).toBe(length); - }); + it.each([6, 8, 12, 16, 32])( + "should generate a hash string respecting parameter %d length", + (length) => { + const hash = processValue(`\${hash:${length}}`, {}, mockSchema); + expect(hash).toMatch(/^[A-Za-z0-9]+$/); + expect(hash.length).toBe(length); + }, + ); }); describe("${uuid}", () => { it("should generate a UUID string", () => { const uuid = processValue("${uuid}", {}, mockSchema); - expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + expect(uuid).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, + ); }); }); @@ -100,8 +114,12 @@ describe("helpers functions", () => { expect(timestamp.length).toBe(nowLength); }); it("should generate a timestamp string in milliseconds from parameter", () => { - const timestamp = processValue("${timestampms:2025-01-01}", {}, mockSchema); - expect(timestamp).toEqual('1735689600000'); + const timestamp = processValue( + "${timestampms:2025-01-01}", + {}, + mockSchema, + ); + expect(timestamp).toEqual("1735689600000"); }); }); describe("${timestamps}", () => { @@ -112,8 +130,12 @@ describe("helpers functions", () => { expect(timestamps.length).toBe(nowLength); }); it("should generate a timestamp string in seconds from parameter", () => { - const timestamps = processValue("${timestamps:2025-01-01}", {}, mockSchema); - expect(timestamps).toEqual('1735689600'); + const timestamps = processValue( + "${timestamps:2025-01-01}", + {}, + mockSchema, + ); + expect(timestamps).toEqual("1735689600"); }); }); @@ -146,39 +168,50 @@ describe("helpers functions", () => { const parts = jwt.split(".") as JWTParts; const decodedPayload = jwtBase64Decode(parts[1]); jwtCheckHeader(parts[0]); - expect (decodedPayload).toHaveProperty("iat"); - expect (decodedPayload).toHaveProperty("iss"); - expect (decodedPayload).toHaveProperty("exp"); - expect (decodedPayload.iss).toEqual("dokploy"); - }); - it.each([6,8,12,16,32])("should generate a random hex string from parameter %d byte length", (length) => { - const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema); - expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/); - expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length - expect(jwt.length).toBeLessThanOrEqual(length * 2); + expect(decodedPayload).toHaveProperty("iat"); + expect(decodedPayload).toHaveProperty("iss"); + expect(decodedPayload).toHaveProperty("exp"); + expect(decodedPayload.iss).toEqual("dokploy"); }); + it.each([6, 8, 12, 16, 32])( + "should generate a random hex string from parameter %d byte length", + (length) => { + const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema); + expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/); + expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length + expect(jwt.length).toBeLessThanOrEqual(length * 2); + }, + ); }); describe("${jwt:secret}", () => { it("should generate a JWT string respecting parameter secret from variable", () => { - const jwt = processValue("${jwt:secret}", {secret: "mysecret"}, mockSchema); + const jwt = processValue( + "${jwt:secret}", + { secret: "mysecret" }, + mockSchema, + ); expect(jwt).toMatch(jwtMatchExp); const parts = jwt.split(".") as JWTParts; const decodedPayload = jwtBase64Decode(parts[1]); jwtCheckHeader(parts[0]); - expect (decodedPayload).toHaveProperty("iat"); - expect (decodedPayload).toHaveProperty("iss"); - expect (decodedPayload).toHaveProperty("exp"); - expect (decodedPayload.iss).toEqual("dokploy"); + expect(decodedPayload).toHaveProperty("iat"); + expect(decodedPayload).toHaveProperty("iss"); + expect(decodedPayload).toHaveProperty("exp"); + expect(decodedPayload.iss).toEqual("dokploy"); }); }); describe("${jwt:secret:payload}", () => { it("should generate a JWT string respecting parameters secret and payload from variables", () => { const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000); const expiry = iat + 3600; - const jwt = processValue("${jwt:secret:payload}", { - secret: "mysecret", - payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`, - }, mockSchema); + const jwt = processValue( + "${jwt:secret:payload}", + { + secret: "mysecret", + payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`, + }, + mockSchema, + ); expect(jwt).toMatch(jwtMatchExp); const parts = jwt.split(".") as JWTParts; jwtCheckHeader(parts[0]); @@ -191,7 +224,9 @@ describe("helpers functions", () => { expect(decodedPayload.exp).toEqual(expiry); expect(decodedPayload).toHaveProperty("customprop"); expect(decodedPayload.customprop).toEqual("customvalue"); - expect(jwt).toEqual("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI") + expect(jwt).toEqual( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI", + ); }); }); -}); \ No newline at end of file +}); diff --git a/packages/server/src/templates/index.ts b/packages/server/src/templates/index.ts index 0d0f87ce..083b90bf 100644 --- a/packages/server/src/templates/index.ts +++ b/packages/server/src/templates/index.ts @@ -9,7 +9,7 @@ import { fetchTemplateFiles } from "./github"; export interface Schema { serverIp: string; projectName: string; -}; +} export type DomainSchema = Pick & { path?: string; @@ -22,7 +22,7 @@ export interface Template { content: string; }>; domains: DomainSchema[]; -}; +} export interface GenerateJWTOptions { length?: number; @@ -65,17 +65,16 @@ export const generatePassword = (quantity = 16): string => { */ export function generateBase64(bytes = 32): string { return randomBytes(bytes).toString("base64"); -}; +} function safeBase64(str: string): string { - return str - .replace(/=/g, "") - .replace(/\+/g, "-") - .replace(/\//g, "_"); -}; + return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); +} function objToJWTBase64(obj: any): string { - return safeBase64(Buffer.from(JSON.stringify(obj), "utf8").toString("base64")); -}; + return safeBase64( + Buffer.from(JSON.stringify(obj), "utf8").toString("base64"), + ); +} export function generateJwt(options: GenerateJWTOptions = {}): string { let { length, secret, payload = {} } = options; @@ -88,16 +87,21 @@ export function generateJwt(options: GenerateJWTOptions = {}): string { }); payload.iss || (payload.iss = "dokploy"); payload.iat || (payload.iat = Math.floor(Date.now() / 1000)); - payload.exp || (payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000)); + payload.exp || + (payload.exp = Math.floor( + new Date("2030-01-01T00:00:00Z").getTime() / 1000, + )); const encodedPayload = objToJWTBase64({ iat: Math.floor(Date.now() / 1000), exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000), ...payload, }); secret || (secret = randomBytes(32).toString("hex")); - const signature = safeBase64(createHmac("SHA256", secret) - .update(`${encodedHeader}.${encodedPayload}`) - .digest("base64")); + const signature = safeBase64( + createHmac("SHA256", secret) + .update(`${encodedHeader}.${encodedPayload}`) + .digest("base64"), + ); return `${encodedHeader}.${encodedPayload}.${signature}`; } diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index ff8fe277..5d9270aa 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -117,7 +117,9 @@ export function processValue( return new Date(varName.slice(12)).getTime().toString(); } if (varName.startsWith("timestamps:")) { - return Math.round(new Date(varName.slice(11)).getTime() / 1000).toString(); + return Math.round( + new Date(varName.slice(11)).getTime() / 1000, + ).toString(); } if (varName === "randomPort") { @@ -129,15 +131,19 @@ export function processValue( } if (varName.startsWith("jwt:")) { - const params:string[] = varName.split(":").slice(1); + const params: string[] = varName.split(":").slice(1); if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) { - return generateJwt({length: Number.parseInt(params[0], 10)}); + return generateJwt({ length: Number.parseInt(params[0], 10) }); } let [secret, payload] = params; if (typeof payload === "string" && variables[payload]) { payload = variables[payload]; } - if (typeof payload === "string" && payload.startsWith("{") && payload.endsWith("}")) { + if ( + typeof payload === "string" && + payload.startsWith("{") && + payload.endsWith("}") + ) { try { payload = JSON.parse(payload); } catch (e) { @@ -146,12 +152,12 @@ export function processValue( console.error("Invalid JWT payload", e); } } - if (typeof payload !== 'object') { + if (typeof payload !== "object") { payload = undefined; } return generateJwt({ - secret: secret ? (variables[secret] || secret) : undefined, - payload: payload as any + secret: secret ? variables[secret] || secret : undefined, + payload: payload as any, }); }