mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-18 20:35:24 +02:00
Compare commits
4 Commits
v0.29.12
...
fix/log-vi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9817da45b | ||
|
|
faf600bd88 | ||
|
|
7ba9818894 | ||
|
|
f577778667 |
26
apps/dokploy/__test__/api/api-key-name.test.ts
Normal file
26
apps/dokploy/__test__/api/api-key-name.test.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { API_KEY_NAME_MAX_LENGTH, apiKeyNameSchema } from "@/lib/api-keys";
|
||||||
|
|
||||||
|
describe("apiKeyNameSchema", () => {
|
||||||
|
it("rejects an empty name", () => {
|
||||||
|
const result = apiKeyNameSchema.safeParse("");
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts a name at the maximum length", () => {
|
||||||
|
const name = "a".repeat(API_KEY_NAME_MAX_LENGTH);
|
||||||
|
const result = apiKeyNameSchema.safeParse(name);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a name over the maximum length instead of passing it to better-auth", () => {
|
||||||
|
const name = "a".repeat(API_KEY_NAME_MAX_LENGTH + 1);
|
||||||
|
const result = apiKeyNameSchema.safeParse(name);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
if (!result.success) {
|
||||||
|
expect(result.error.issues[0]?.message).toBe(
|
||||||
|
`Name must be at most ${API_KEY_NAME_MAX_LENGTH} characters`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
31
apps/dokploy/__test__/logs/log-classification.test.ts
Normal file
31
apps/dokploy/__test__/logs/log-classification.test.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { getLogType } from "@/components/dashboard/docker/logs/utils";
|
||||||
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
test("classifies real failures as error", () => {
|
||||||
|
expect(getLogType("Error: connection refused at db:5432").type).toBe("error");
|
||||||
|
expect(getLogType("[ERROR] something went wrong").type).toBe("error");
|
||||||
|
expect(getLogType("Deployment failed").type).toBe("error");
|
||||||
|
expect(
|
||||||
|
getLogType(
|
||||||
|
'NOTICE [Job "sync-1m" (e1305c5b54b1)] Finished in "326ms", failed: true, skipped: false, error: exit code 1',
|
||||||
|
).type,
|
||||||
|
).toBe("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not classify explicit non-error key/values as error (#4538)", () => {
|
||||||
|
// ofelia job-completion summary for a successful run
|
||||||
|
expect(
|
||||||
|
getLogType(
|
||||||
|
'NOTICE [Job "sync-1m" (e1305c5b54b1)] Finished in "326.16795ms", failed: false, skipped: false, error: none',
|
||||||
|
).type,
|
||||||
|
).not.toBe("error");
|
||||||
|
|
||||||
|
expect(getLogType("request done, error: null").type).not.toBe("error");
|
||||||
|
expect(getLogType("checks passed, failures=0").type).not.toBe("error");
|
||||||
|
expect(getLogType('shutdown clean, error=""').type).not.toBe("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps statusCode-based classification", () => {
|
||||||
|
expect(getLogType('{"statusCode": "500"}').type).toBe("error");
|
||||||
|
expect(getLogType('{"statusCode": "204"}').type).toBe("success");
|
||||||
|
});
|
||||||
@@ -97,17 +97,23 @@ export const getLogType = (message: string): LogStyle => {
|
|||||||
return LOG_STYLES.info;
|
return LOG_STYLES.info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Key/value pairs that explicitly report a non-error (e.g. "error: none",
|
||||||
|
// "failed: false") must not trigger the error keyword patterns below
|
||||||
|
const nonErrorKeyValues =
|
||||||
|
/\b(?:error|err|errors|failed|failure|failures)s?\s*[:=]\s*(?:none|null|nil|false|0|no|-|""|'')(?=[\s,;.)\]]|$)/gi;
|
||||||
|
const errorScope = lowerMessage.replace(nonErrorKeyValues, "");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
/(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) ||
|
/(?:^|\s)(?:error|err):?\s/i.test(errorScope) ||
|
||||||
/\b(?:exception|failed|failure)\b/i.test(lowerMessage) ||
|
/\b(?:exception|failed|failure)\b/i.test(errorScope) ||
|
||||||
/(?:stack\s?trace):\s*$/i.test(lowerMessage) ||
|
/(?:stack\s?trace):\s*$/i.test(errorScope) ||
|
||||||
/^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(lowerMessage) ||
|
/^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(errorScope) ||
|
||||||
/\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(lowerMessage) ||
|
/\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(errorScope) ||
|
||||||
/Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(lowerMessage) ||
|
/Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(errorScope) ||
|
||||||
/\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(lowerMessage) ||
|
/\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(errorScope) ||
|
||||||
/\[(?:error|err|fatal)\]/i.test(lowerMessage) ||
|
/\[(?:error|err|fatal)\]/i.test(errorScope) ||
|
||||||
/\b(?:crash|critical|fatal)\b/i.test(lowerMessage) ||
|
/\b(?:crash|critical|fatal)\b/i.test(errorScope) ||
|
||||||
/\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(lowerMessage)
|
/\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(errorScope)
|
||||||
) {
|
) {
|
||||||
return LOG_STYLES.error;
|
return LOG_STYLES.error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,10 +32,11 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { API_KEY_NAME_MAX_LENGTH, apiKeyNameSchema } from "@/lib/api-keys";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: apiKeyNameSchema,
|
||||||
prefix: z.string().optional(),
|
prefix: z.string().optional(),
|
||||||
expiresIn: z.number().nullable(),
|
expiresIn: z.number().nullable(),
|
||||||
organizationId: z.string().min(1, "Organization is required"),
|
organizationId: z.string().min(1, "Organization is required"),
|
||||||
@@ -159,8 +160,15 @@ export const AddApiKey = () => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="My API Key" {...field} />
|
<Input
|
||||||
|
placeholder="My API Key"
|
||||||
|
maxLength={API_KEY_NAME_MAX_LENGTH}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Maximum {API_KEY_NAME_MAX_LENGTH} characters
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
23
apps/dokploy/lib/api-keys.ts
Normal file
23
apps/dokploy/lib/api-keys.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum length allowed for an API key name.
|
||||||
|
*
|
||||||
|
* This mirrors the default `maximumNameLength` enforced by the
|
||||||
|
* `@better-auth/api-key` plugin. Names longer than this are rejected by
|
||||||
|
* better-auth with a 400, so we validate against it up front to surface a
|
||||||
|
* clear field-level error instead of an opaque 500.
|
||||||
|
*/
|
||||||
|
export const API_KEY_NAME_MAX_LENGTH = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared validation for an API key name, used by both the tRPC input schema
|
||||||
|
* and the client form so the two can't drift.
|
||||||
|
*/
|
||||||
|
export const apiKeyNameSchema = z
|
||||||
|
.string()
|
||||||
|
.min(1, "Name is required")
|
||||||
|
.max(
|
||||||
|
API_KEY_NAME_MAX_LENGTH,
|
||||||
|
`Name must be at most ${API_KEY_NAME_MAX_LENGTH} characters`,
|
||||||
|
);
|
||||||
@@ -35,6 +35,7 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
import { and, asc, eq, gt, ne } from "drizzle-orm";
|
import { and, asc, eq, gt, ne } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiKeyNameSchema } from "@/lib/api-keys";
|
||||||
import { audit } from "@/server/api/utils/audit";
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import {
|
import {
|
||||||
adminProcedure,
|
adminProcedure,
|
||||||
@@ -45,7 +46,7 @@ import {
|
|||||||
} from "../trpc";
|
} from "../trpc";
|
||||||
|
|
||||||
const apiCreateApiKey = z.object({
|
const apiCreateApiKey = z.object({
|
||||||
name: z.string().min(1),
|
name: apiKeyNameSchema,
|
||||||
prefix: z.string().optional(),
|
prefix: z.string().optional(),
|
||||||
expiresIn: z.number().optional(),
|
expiresIn: z.number().optional(),
|
||||||
metadata: z.object({
|
metadata: z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user