mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #1749 from malko/template-helpers
Template helpers: Enhanced JWT generation
This commit is contained in:
commit
ade4b8dd1b
@ -51,6 +51,35 @@ describe("processTemplate", () => {
|
|||||||
expect(result.domains).toHaveLength(0);
|
expect(result.domains).toHaveLength(0);
|
||||||
expect(result.mounts).toHaveLength(0);
|
expect(result.mounts).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should allow creation of real jwt secret", () => {
|
||||||
|
const template: CompleteTemplate = {
|
||||||
|
metadata: {} as any,
|
||||||
|
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}",
|
||||||
|
}),
|
||||||
|
anon_key: "${jwt:jwt_secret:anon_payload}",
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
domains: [],
|
||||||
|
env: {
|
||||||
|
ANON_KEY: "${anon_key}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
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.mounts).toHaveLength(0);
|
||||||
|
expect(result.domains).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("domains processing", () => {
|
describe("domains processing", () => {
|
||||||
|
232
apps/dokploy/__test__/templates/helpers.template.test.ts
Normal file
232
apps/dokploy/__test__/templates/helpers.template.test.ts
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
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 = {
|
||||||
|
projectName: "test",
|
||||||
|
serverIp: "127.0.0.1",
|
||||||
|
};
|
||||||
|
// some helpers to test jwt
|
||||||
|
type JWTParts = [string, string, string];
|
||||||
|
const jwtMatchExp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/;
|
||||||
|
const jwtBase64Decode = (str: string) => {
|
||||||
|
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
|
||||||
|
const decoded = Buffer.from(base64 + padding, "base64").toString("utf-8");
|
||||||
|
return JSON.parse(decoded);
|
||||||
|
};
|
||||||
|
const jwtCheckHeader = (jwtHeader: string) => {
|
||||||
|
const decodedHeader = jwtBase64Decode(jwtHeader);
|
||||||
|
expect(decodedHeader).toHaveProperty("alg");
|
||||||
|
expect(decodedHeader).toHaveProperty("typ");
|
||||||
|
expect(decodedHeader.alg).toEqual("HS256");
|
||||||
|
expect(decodedHeader.typ).toEqual("JWT");
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("${domain}", () => {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${base64}", () => {
|
||||||
|
it("should generate a base64 string", () => {
|
||||||
|
const base64 = processValue("${base64}", {}, mockSchema);
|
||||||
|
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
|
||||||
|
});
|
||||||
|
it.each([
|
||||||
|
[4, 8],
|
||||||
|
[8, 12],
|
||||||
|
[16, 24],
|
||||||
|
[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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${password}", () => {
|
||||||
|
it("should generate a password string", () => {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${hash}", () => {
|
||||||
|
it("should generate a hash string", () => {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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}$/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${timestamp}", () => {
|
||||||
|
it("should generate a timestamp string in milliseconds", () => {
|
||||||
|
const timestamp = processValue("${timestamp}", {}, mockSchema);
|
||||||
|
const nowLength = Math.floor(Date.now()).toString().length;
|
||||||
|
expect(timestamp).toMatch(/^\d+$/);
|
||||||
|
expect(timestamp.length).toBe(nowLength);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("${timestampms}", () => {
|
||||||
|
it("should generate a timestamp string in milliseconds", () => {
|
||||||
|
const timestamp = processValue("${timestampms}", {}, mockSchema);
|
||||||
|
const nowLength = Date.now().toString().length;
|
||||||
|
expect(timestamp).toMatch(/^\d+$/);
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("${timestamps}", () => {
|
||||||
|
it("should generate a timestamp string in seconds", () => {
|
||||||
|
const timestamps = processValue("${timestamps}", {}, mockSchema);
|
||||||
|
const nowLength = Math.floor(Date.now() / 1000).toString().length;
|
||||||
|
expect(timestamps).toMatch(/^\d+$/);
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${randomPort}", () => {
|
||||||
|
it("should generate a random port string", () => {
|
||||||
|
const randomPort = processValue("${randomPort}", {}, mockSchema);
|
||||||
|
expect(randomPort).toMatch(/^\d+$/);
|
||||||
|
expect(Number(randomPort)).toBeLessThan(65536);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${username}", () => {
|
||||||
|
it("should generate a username string", () => {
|
||||||
|
const username = processValue("${username}", {}, mockSchema);
|
||||||
|
expect(username).toMatch(/^[a-zA-Z0-9._-]{3,}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${email}", () => {
|
||||||
|
it("should generate an email string", () => {
|
||||||
|
const email = processValue("${email}", {}, mockSchema);
|
||||||
|
expect(email).toMatch(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${jwt}", () => {
|
||||||
|
it("should generate a JWT string", () => {
|
||||||
|
const jwt = processValue("${jwt}", {}, 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");
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
expect(jwt).toMatch(jwtMatchExp);
|
||||||
|
const parts = jwt.split(".") as JWTParts;
|
||||||
|
jwtCheckHeader(parts[0]);
|
||||||
|
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||||
|
expect(decodedPayload).toHaveProperty("iat");
|
||||||
|
expect(decodedPayload.iat).toEqual(iat);
|
||||||
|
expect(decodedPayload).toHaveProperty("iss");
|
||||||
|
expect(decodedPayload.iss).toEqual("test-issuer");
|
||||||
|
expect(decodedPayload).toHaveProperty("exp");
|
||||||
|
expect(decodedPayload.exp).toEqual(expiry);
|
||||||
|
expect(decodedPayload).toHaveProperty("customprop");
|
||||||
|
expect(decodedPayload.customprop).toEqual("customvalue");
|
||||||
|
expect(jwt).toEqual(
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,4 +1,4 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes, createHmac } from "node:crypto";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
@ -24,6 +24,12 @@ export interface Template {
|
|||||||
domains: DomainSchema[];
|
domains: DomainSchema[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GenerateJWTOptions {
|
||||||
|
length?: number;
|
||||||
|
secret?: string;
|
||||||
|
payload?: Record<string, unknown> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const generateRandomDomain = ({
|
export const generateRandomDomain = ({
|
||||||
serverIp,
|
serverIp,
|
||||||
projectName,
|
projectName,
|
||||||
@ -61,8 +67,48 @@ export function generateBase64(bytes = 32): string {
|
|||||||
return randomBytes(bytes).toString("base64");
|
return randomBytes(bytes).toString("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateJwt(length = 256): string {
|
function safeBase64(str: string): string {
|
||||||
|
return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
||||||
|
}
|
||||||
|
function objToJWTBase64(obj: any): string {
|
||||||
|
return safeBase64(
|
||||||
|
Buffer.from(JSON.stringify(obj), "utf8").toString("base64"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateJwt(options: GenerateJWTOptions = {}): string {
|
||||||
|
let { length, secret, payload = {} } = options;
|
||||||
|
if (length) {
|
||||||
return randomBytes(length).toString("hex");
|
return randomBytes(length).toString("hex");
|
||||||
|
}
|
||||||
|
const encodedHeader = objToJWTBase64({
|
||||||
|
alg: "HS256",
|
||||||
|
typ: "JWT",
|
||||||
|
});
|
||||||
|
if (!payload.iss) {
|
||||||
|
payload.iss = "dokploy";
|
||||||
|
}
|
||||||
|
if (!payload.iat) {
|
||||||
|
payload.iat = Math.floor(Date.now() / 1000);
|
||||||
|
}
|
||||||
|
if (!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,
|
||||||
|
});
|
||||||
|
if (!secret) {
|
||||||
|
secret = randomBytes(32).toString("hex");
|
||||||
|
}
|
||||||
|
const signature = safeBase64(
|
||||||
|
createHmac("SHA256", secret)
|
||||||
|
.update(`${encodedHeader}.${encodedPayload}`)
|
||||||
|
.digest("base64"),
|
||||||
|
);
|
||||||
|
|
||||||
|
return `${encodedHeader}.${encodedPayload}.${signature}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -65,7 +65,7 @@ export interface Template {
|
|||||||
/**
|
/**
|
||||||
* Process a string value and replace variables
|
* Process a string value and replace variables
|
||||||
*/
|
*/
|
||||||
function processValue(
|
export function processValue(
|
||||||
value: string,
|
value: string,
|
||||||
variables: Record<string, string>,
|
variables: Record<string, string>,
|
||||||
schema: Schema,
|
schema: Schema,
|
||||||
@ -84,11 +84,11 @@ function processValue(
|
|||||||
const length = Number.parseInt(varName.split(":")[1], 10) || 32;
|
const length = Number.parseInt(varName.split(":")[1], 10) || 32;
|
||||||
return generateBase64(length);
|
return generateBase64(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (varName.startsWith("password:")) {
|
if (varName.startsWith("password:")) {
|
||||||
const length = Number.parseInt(varName.split(":")[1], 10) || 16;
|
const length = Number.parseInt(varName.split(":")[1], 10) || 16;
|
||||||
return generatePassword(length);
|
return generatePassword(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (varName === "password") {
|
if (varName === "password") {
|
||||||
return generatePassword(16);
|
return generatePassword(16);
|
||||||
}
|
}
|
||||||
@ -97,14 +97,31 @@ function processValue(
|
|||||||
const length = Number.parseInt(varName.split(":")[1], 10) || 8;
|
const length = Number.parseInt(varName.split(":")[1], 10) || 8;
|
||||||
return generateHash(length);
|
return generateHash(length);
|
||||||
}
|
}
|
||||||
|
if (varName === "hash") {
|
||||||
|
return generateHash();
|
||||||
|
}
|
||||||
|
|
||||||
if (varName === "uuid") {
|
if (varName === "uuid") {
|
||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (varName === "timestamp") {
|
if (varName === "timestamp" || varName === "timestampms") {
|
||||||
return Date.now().toString();
|
return Date.now().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (varName === "timestamps") {
|
||||||
|
return Math.round(Date.now() / 1000).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (varName.startsWith("timestampms:")) {
|
||||||
|
return new Date(varName.slice(12)).getTime().toString();
|
||||||
|
}
|
||||||
|
if (varName.startsWith("timestamps:")) {
|
||||||
|
return Math.round(
|
||||||
|
new Date(varName.slice(11)).getTime() / 1000,
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
if (varName === "randomPort") {
|
if (varName === "randomPort") {
|
||||||
return Math.floor(Math.random() * 65535).toString();
|
return Math.floor(Math.random() * 65535).toString();
|
||||||
}
|
}
|
||||||
@ -114,8 +131,34 @@ function processValue(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (varName.startsWith("jwt:")) {
|
if (varName.startsWith("jwt:")) {
|
||||||
const length = Number.parseInt(varName.split(":")[1], 10) || 256;
|
const params: string[] = varName.split(":").slice(1);
|
||||||
return generateJwt(length);
|
if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) {
|
||||||
|
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("}")
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(payload);
|
||||||
|
} catch (e) {
|
||||||
|
// If payload is not a valid JSON, invalid it
|
||||||
|
payload = undefined;
|
||||||
|
console.error("Invalid JWT payload", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof payload !== "object") {
|
||||||
|
payload = undefined;
|
||||||
|
}
|
||||||
|
return generateJwt({
|
||||||
|
secret: secret ? variables[secret] || secret : undefined,
|
||||||
|
payload: payload as any,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (varName === "username") {
|
if (varName === "username") {
|
||||||
@ -147,7 +190,7 @@ export function processVariables(
|
|||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
const variables: Record<string, string> = {};
|
const variables: Record<string, string> = {};
|
||||||
|
|
||||||
// First pass: Process variables that don't depend on other variables
|
// First pass: Process some variables that don't depend on other variables
|
||||||
for (const [key, value] of Object.entries(template.variables)) {
|
for (const [key, value] of Object.entries(template.variables)) {
|
||||||
if (typeof value !== "string") continue;
|
if (typeof value !== "string") continue;
|
||||||
|
|
||||||
@ -161,6 +204,8 @@ export function processVariables(
|
|||||||
const match = value.match(/\${password:(\d+)}/);
|
const match = value.match(/\${password:(\d+)}/);
|
||||||
const length = match?.[1] ? Number.parseInt(match[1], 10) : 16;
|
const length = match?.[1] ? Number.parseInt(match[1], 10) : 16;
|
||||||
variables[key] = generatePassword(length);
|
variables[key] = generatePassword(length);
|
||||||
|
} else if (value === "${hash}") {
|
||||||
|
variables[key] = generateHash();
|
||||||
} else if (value.startsWith("${hash:")) {
|
} else if (value.startsWith("${hash:")) {
|
||||||
const match = value.match(/\${hash:(\d+)}/);
|
const match = value.match(/\${hash:(\d+)}/);
|
||||||
const length = match?.[1] ? Number.parseInt(match[1], 10) : 8;
|
const length = match?.[1] ? Number.parseInt(match[1], 10) : 8;
|
||||||
|
Loading…
Reference in New Issue
Block a user