Compare commits

..

1 Commits

Author SHA1 Message Date
Henry
d3687cfb10 fix: API key authentication and REST handler errors 2026-03-18 22:46:30 +00:00
9 changed files with 25 additions and 135 deletions

View File

@@ -28,7 +28,6 @@ const config = {
], ],
}, },
/** Enables hot reloading for local packages without a build step */ /** Enables hot reloading for local packages without a build step */
transpilePackages: [ transpilePackages: [
"@kan/api", "@kan/api",
@@ -66,8 +65,6 @@ const config = {
}, },
}, },
}, },
serverExternalPackages: ["pino"],
experimental: { experimental: {
// instrumentationHook: true, // instrumentationHook: true,
swcPlugins: [["@lingui/swc-plugin", {}]], swcPlugins: [["@lingui/swc-plugin", {}]],

View File

@@ -39,7 +39,7 @@ export function LabelForm({
labelPublicId: entityId, labelPublicId: entityId,
}, },
{ {
enabled: !!isEdit && entityId.length >= 12, enabled: isEdit && !!entityId,
}, },
); );

View File

@@ -39,8 +39,6 @@ services:
# Logging (optional - debug, info, warn, error; defaults to debug in dev, info in prod) # Logging (optional - debug, info, warn, error; defaults to debug in dev, info in prod)
- LOG_LEVEL=${LOG_LEVEL} - LOG_LEVEL=${LOG_LEVEL}
- AXIOM_TOKEN=${AXIOM_TOKEN}
- AXIOM_DATASET=${AXIOM_DATASET}
# Stripe # Stripe
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}

View File

@@ -227,7 +227,7 @@ export const boardRouter = createTRPCRouter({
boardSlug: z boardSlug: z
.string() .string()
.min(3) .min(3)
.max(60) .max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/), .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
members: z.array(z.string().min(12)).optional(), members: z.array(z.string().min(12)).optional(),
labels: z.array(z.string().min(12)).optional(), labels: z.array(z.string().min(12)).optional(),
@@ -657,7 +657,7 @@ export const boardRouter = createTRPCRouter({
boardSlug: z boardSlug: z
.string() .string()
.min(3) .min(3)
.max(60) .max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/), .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
boardPublicId: z.string().min(12), boardPublicId: z.string().min(12),
}), }),

View File

@@ -1,4 +1,3 @@
import { randomUUID } from "crypto";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import type { NextApiRequest } from "next"; import type { NextApiRequest } from "next";
import type { OpenApiMeta } from "trpc-to-openapi"; import type { OpenApiMeta } from "trpc-to-openapi";
@@ -12,28 +11,7 @@ import { initAuth } from "@kan/auth/server";
import { createDrizzleClient } from "@kan/db/client"; import { createDrizzleClient } from "@kan/db/client";
import { createLogger } from "@kan/logger"; import { createLogger } from "@kan/logger";
const log = createLogger("api"); const log = createLogger("trpc");
const TRPC_STATUS_MAP: Partial<Record<TRPCError["code"], number>> = {
PARSE_ERROR: 400,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_SUPPORTED: 405,
TIMEOUT: 408,
CONFLICT: 409,
PRECONDITION_FAILED: 412,
PAYLOAD_TOO_LARGE: 413,
UNPROCESSABLE_CONTENT: 422,
TOO_MANY_REQUESTS: 429,
CLIENT_CLOSED_REQUEST: 499,
INTERNAL_SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
};
export interface User { export interface User {
id: string; id: string;
@@ -72,7 +50,6 @@ interface CreateContextOptions {
db: dbClient; db: dbClient;
auth: ReturnType<typeof createAuthWithHeaders>; auth: ReturnType<typeof createAuthWithHeaders>;
headers: Headers; headers: Headers;
transport?: "trpc" | "rest";
} }
export const createInnerTRPCContext = (opts: CreateContextOptions) => { export const createInnerTRPCContext = (opts: CreateContextOptions) => {
@@ -81,8 +58,6 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => {
db: opts.db, db: opts.db,
auth: opts.auth, auth: opts.auth,
headers: opts.headers, headers: opts.headers,
transport: opts.transport ?? "trpc",
requestId: randomUUID(),
}; };
}; };
@@ -94,13 +69,7 @@ export const createTRPCContext = async ({ req }: CreateNextContextOptions) => {
const session = await auth.api.getSession(); const session = await auth.api.getSession();
return createInnerTRPCContext({ return createInnerTRPCContext({ db, user: session?.user, auth, headers });
db,
user: session?.user,
auth,
headers,
transport: "trpc",
});
}; };
export const createNextApiContext = async (req: NextApiRequest) => { export const createNextApiContext = async (req: NextApiRequest) => {
@@ -111,13 +80,7 @@ export const createNextApiContext = async (req: NextApiRequest) => {
const session = await auth.api.getSession(); const session = await auth.api.getSession();
return createInnerTRPCContext({ return createInnerTRPCContext({ db, user: session?.user, auth, headers });
db,
user: session?.user,
auth,
headers,
transport: "trpc",
});
}; };
export const createRESTContext = async ({ req }: CreateNextContextOptions) => { export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
@@ -130,16 +93,11 @@ export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
try { try {
session = await auth.api.getSession(); session = await auth.api.getSession();
} catch (error) { } catch (error) {
log.warn({ err: error }, "Failed to get session, treating as unauthenticated"); log.error({ err: error }, "Error getting session");
throw error;
} }
return createInnerTRPCContext({ return createInnerTRPCContext({ db, user: session?.user, auth, headers });
db,
user: session?.user,
auth,
headers,
transport: "rest",
});
}; };
const t = initTRPC const t = initTRPC
@@ -168,33 +126,12 @@ const loggingMiddleware = t.middleware(async ({ path, type, next, ctx }) => {
const result = await next(); const result = await next();
const duration = Date.now() - start; const duration = Date.now() - start;
const { user, transport, requestId } = ctx as { const meta = { procedure: path, type, duration, userId: (ctx as { user?: { id: string } }).user?.id };
user?: { id: string; email: string };
transport?: string;
requestId?: string;
};
const isCloud = process.env.NEXT_PUBLIC_KAN_ENV === "cloud";
const meta = {
requestId,
procedure: path,
type,
transport,
duration,
userId: user?.id,
...(isCloud && { email: user?.email }),
};
const label = transport === "rest" ? "REST" : "tRPC";
if (result.ok) { if (result.ok) {
log.info({ ...meta, status: 200 }, `${label} OK`); log.info(meta, "tRPC OK");
} else { } else {
const status = TRPC_STATUS_MAP[result.error.code] ?? 500; log.error({ ...meta, err: result.error }, "tRPC error");
const errorCode = result.error.code;
log.error(
{ ...meta, status, errorCode, err: result.error },
`${label} error`,
);
} }
return result; return result;

View File

@@ -19,7 +19,6 @@
"typecheck": "tsc --noEmit --emitDeclarationOnly false" "typecheck": "tsc --noEmit --emitDeclarationOnly false"
}, },
"dependencies": { "dependencies": {
"@axiomhq/js": "^1.4.0",
"pino": "^9.14.0", "pino": "^9.14.0",
"pino-pretty": "^13.1.3" "pino-pretty": "^13.1.3"
}, },

View File

@@ -1,43 +1,20 @@
import { Axiom } from "@axiomhq/js";
import pino from "pino"; import pino from "pino";
const isDev = process.env.NODE_ENV !== "production"; const isDev = process.env.NODE_ENV !== "production";
const isCloud = process.env.NEXT_PUBLIC_KAN_ENV === "cloud";
const level = process.env.LOG_LEVEL || (isDev ? "debug" : "info"); const level = process.env.LOG_LEVEL || (isDev ? "debug" : "info");
const axiomToken = process.env.AXIOM_TOKEN; export const logger = pino({
const axiomDataset = process.env.AXIOM_DATASET; level,
const useAxiom = isCloud && !!axiomToken && !!axiomDataset; ...(isDev && {
transport: {
function createAxiomStream(token: string, dataset: string): pino.DestinationStream { target: "pino-pretty",
const client = new Axiom({ token }); options: {
return { colorize: true,
write(msg: string) { ignore: "pid,hostname",
try { translateTime: "HH:MM:ss",
client.ingest(dataset, [JSON.parse(msg) as Record<string, unknown>]); },
} catch {
// ignore malformed log lines
}
}, },
}; }),
} });
export const logger = useAxiom
? pino(
{ level },
pino.multistream([
{ stream: process.stdout, level },
{ stream: createAxiomStream(axiomToken, axiomDataset), level },
]),
)
: pino({
level,
...(isDev && {
transport: {
target: "pino-pretty",
options: { colorize: true, ignore: "pid,hostname", translateTime: "HH:MM:ss" },
},
}),
});
export const createLogger = (module: string) => logger.child({ module }); export const createLogger = (module: string) => logger.child({ module });

16
pnpm-lock.yaml generated
View File

@@ -504,9 +504,6 @@ importers:
packages/logger: packages/logger:
dependencies: dependencies:
'@axiomhq/js':
specifier: ^1.4.0
version: 1.4.0
pino: pino:
specifier: ^9.14.0 specifier: ^9.14.0
version: 9.14.0 version: 9.14.0
@@ -894,10 +891,6 @@ packages:
resolution: {integrity: sha512-kLO7k7cGJ6KaHiExSJWojZurF7SnGMDHXRuQunFnEoD0n1yB6Lqy/S/zHiQ7oJnBhPr9q0TW9qFkrsZb1Uc54w==} resolution: {integrity: sha512-kLO7k7cGJ6KaHiExSJWojZurF7SnGMDHXRuQunFnEoD0n1yB6Lqy/S/zHiQ7oJnBhPr9q0TW9qFkrsZb1Uc54w==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@axiomhq/js@1.4.0':
resolution: {integrity: sha512-wC5x1ud/QJMstrjpicATkyY8+ZVWEl4WlXMtA5EZf7hkj0+b191yv4yynLxLEfr/MveXora9m6CWdJq4DsbcAg==}
engines: {node: '>=20'}
'@babel/code-frame@7.27.1': '@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -5635,9 +5628,6 @@ packages:
picomatch: picomatch:
optional: true optional: true
fetch-retry@6.0.0:
resolution: {integrity: sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==}
fflate@0.4.8: fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
@@ -9394,10 +9384,6 @@ snapshots:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@axiomhq/js@1.4.0':
dependencies:
fetch-retry: 6.0.0
'@babel/code-frame@7.27.1': '@babel/code-frame@7.27.1':
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.27.1 '@babel/helper-validator-identifier': 7.27.1
@@ -14417,8 +14403,6 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.3 picomatch: 4.0.3
fetch-retry@6.0.0: {}
fflate@0.4.8: {} fflate@0.4.8: {}
figures@3.2.0: figures@3.2.0:

View File

@@ -130,9 +130,7 @@
"NOVU_API_KEY", "NOVU_API_KEY",
"EMAIL_UNSUBSCRIBE_SECRET", "EMAIL_UNSUBSCRIBE_SECRET",
"REDIS_URL", "REDIS_URL",
"LOG_LEVEL", "LOG_LEVEL"
"AXIOM_TOKEN",
"AXIOM_DATASET"
], ],
"globalPassThroughEnv": [ "globalPassThroughEnv": [
"NODE_ENV", "NODE_ENV",