Merge branch 'main' into trpc-sentry-middleware

This commit is contained in:
Puru Dahal
2024-07-06 23:17:21 -05:00
committed by GitHub
11 changed files with 146 additions and 99 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
"db:migrate": "prisma migrate dev",
"db:generate": "prisma generate",
"db:seed": "NODE_ENV='test' tsx prisma/seeds/index.ts",
"dev": "next dev | pino-pretty",
"dev": "next dev",
"build": "next build",
"start": "next start",
"postinstall": "prisma generate",
@@ -3,9 +3,12 @@
import { dayjsExt } from "@/common/dayjs";
import Tldr from "@/components/common/tldr";
import { Card } from "@/components/ui/card";
import { api } from "@/trpc/react";
import { RiMore2Fill } from "@remixicon/react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { ApiKeyAlertDialog } from "@/components/apiKey/apiKey-alert-dialog";
import { ConfirmDialog } from "@/components/common/confirmDialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -30,6 +33,28 @@ interface ApiKey {
}
const ApiKeysTable = ({ keys }: { keys: ApiKey[] }) => {
const router = useRouter();
const deleteMutation = api.apiKey.delete.useMutation({
onSuccess: ({ message }) => {
toast.success(message);
},
onError: (error) => {
console.error(error);
toast.error("An error occurred while creating the API key.");
},
onSettled: () => {
router.refresh();
},
});
const dialogTitle = "Are you sure?";
const dialogBody =
"Are you sure you want to delete this key? This action cannot be undone and you will loose the access if this API key is currently being used.";
const dialogTrigger = <div className="text-rose-600">Delete key</div>;
return (
<Card className="mx-auto mt-3 w-[28rem] sm:w-[38rem] md:w-full">
<div className="mx-3">
@@ -78,7 +103,15 @@ const ApiKeysTable = ({ keys }: { keys: ApiKey[] }) => {
Rotate key
</DropdownMenuItem>
<ApiKeyAlertDialog keyId={key.keyId} key={key.keyId} />
<ConfirmDialog
key={key.keyId}
title={dialogTitle}
body={dialogBody}
trigger={dialogTrigger}
onConfirm={() => {
deleteMutation.mutate({ keyId: key.keyId });
}}
/>
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -1,73 +0,0 @@
"use client";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { api } from "@/trpc/react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import React from "react";
interface ApiKeyAlertDialogProps {
keyId: string;
}
export function ApiKeyAlertDialog({ keyId }: ApiKeyAlertDialogProps) {
const router = useRouter();
const deleteMutation = api.apiKey.delete.useMutation({
onSuccess: ({ message }) => {
toast.success(message);
},
onError: (error) => {
console.error(error);
toast.error("An error occurred while creating the API key.");
},
onSettled: () => {
router.refresh();
},
});
return (
<AlertDialog>
<AlertDialogTrigger
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-zinc-100"
asChild={true}
>
<div className="text-rose-600">Delete key</div>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{"Are you absolutely sure?"}</AlertDialogTitle>
<AlertDialogDescription>
{
"Are you sure you want to delete this key? This action cannot be undone and you will loose the access if this you are currently using this API key."
}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
deleteMutation.mutate({ keyId });
}}
>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
+52
View File
@@ -0,0 +1,52 @@
"use client";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import type React from "react";
interface ConfirmDialogProps {
title: string;
body: string;
trigger: React.ReactNode;
onConfirm: () => void;
onCancel?: () => void;
}
export function ConfirmDialog({
title,
body,
trigger,
onConfirm,
onCancel,
}: ConfirmDialogProps) {
return (
<AlertDialog>
<AlertDialogTrigger
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-zinc-100"
asChild={true}
>
{trigger}
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{body}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
+5 -1
View File
@@ -30,8 +30,12 @@ export const env = createEnv({
"You forgot to change the default URL",
),
NODE_ENV: z
.enum(["development", "test", "production"])
.enum(["development", "test", "production", "staging"])
.default("development"),
LOGS: z
.string()
.default("0")
.transform((s) => s !== "false" && s !== "0"),
NEXTAUTH_URL: z.string(),
NEXTAUTH_SECRET: z.string(),
EMAIL_SERVER: z.string().optional(),
+25 -1
View File
@@ -1,10 +1,13 @@
import type { JOB_TYPES } from "@/constants/job";
import { env } from "@/env";
import { logger } from "@/lib/logger";
import { singleton } from "@/lib/singleton";
import pgBoss from "pg-boss";
type JobTypes = typeof JOB_TYPES;
const log = logger.child({ module: "jobs" });
export type JobType = {
[Key in keyof JobTypes]: `${Key}.${JobTypes[Key][number]}`;
}[keyof JobTypes];
@@ -59,9 +62,30 @@ export class JobManager {
}
async start(): Promise<void> {
const startTime = Date.now();
log.info("Starting pg-boss job queue manager");
await this.boss.start();
for (const job of this.jobs.values()) {
const endTime = Date.now();
const elapsedTime = endTime - startTime;
log.info(
`Successfully started pg-boss job queue manager in ${elapsedTime}ms`,
);
for (const [jobName, job] of this.jobs.entries()) {
const jobStartTime = Date.now();
log.info(`Registering pg-boss job:${jobName}`);
await job.start();
const jobEndTime = Date.now();
const jobElapsedTime = jobEndTime - jobStartTime;
log.info(
`Successfully registered pg-boss job:${jobName} in ${jobElapsedTime}ms`,
);
}
}
}
+9 -7
View File
@@ -1,11 +1,9 @@
import { env } from "@/env";
import pino, { type Logger } from "pino";
export const logger: Logger =
process.env.NODE_ENV === "production"
? // JSON in production
pino({ level: "info" })
: // Pretty print in development
pino({
export const logger: Logger = pino({
...(env.NODE_ENV === "development"
? {
transport: {
target: "pino-pretty",
options: {
@@ -13,4 +11,8 @@ export const logger: Logger =
},
},
level: "debug",
});
}
: {
level: "info",
}),
});
+9 -4
View File
@@ -1,13 +1,18 @@
import { logger } from "@/lib/logger";
import { NextResponse } from "next/server";
import { type NextRequest, userAgent } from "next/server";
import { env } from "./env";
const log = logger.child({ module: "middleware" });
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
const { url, ip, geo, method } = request;
const time = new Date().toISOString();
const { device, browser, isBot } = userAgent(request);
logger.info({ method, time, url, ip, device, browser, geo, isBot });
if (env.LOGS || env.NODE_ENV === "production" || env.NODE_ENV === "staging") {
const { url, ip, geo, method } = request;
const time = new Date().toISOString();
const { device, browser, isBot } = userAgent(request);
log.info({ method, time, url, ip, device, browser, geo, isBot });
}
return NextResponse.next();
}
+5 -3
View File
@@ -4,6 +4,8 @@ import { HTTPException } from "hono/http-exception";
import type { StatusCode } from "hono/utils/http-status";
import { z } from "zod";
const log = logger.child({ module: "api-error" });
const ErrorSchema = z.object({
error: z.object({
code: z.string(),
@@ -117,7 +119,7 @@ export class ApiError extends HTTPException {
export function handleError(err: Error, c: Context): Response {
if (err instanceof ApiError) {
if (err.status >= 500) {
logger.error("ApiError", {
log.error("ApiError", {
name: err.name,
code: err.code,
status: err.status,
@@ -137,7 +139,7 @@ export function handleError(err: Error, c: Context): Response {
if (err instanceof HTTPException) {
if (err.status >= 500) {
logger.error("HTTPException", {
log.error("HTTPException", {
name: err.name,
status: err.status,
message: err.message,
@@ -159,7 +161,7 @@ export function handleError(err: Error, c: Context): Response {
* This is a generic error, we should log it and return a 500
*/
logger.error("UnhandledError", {
log.error("UnhandledError", {
name: err.name,
message: err.message,
cause: err.cause,
+2 -4
View File
@@ -1,10 +1,9 @@
import { PrismaClient } from "@prisma/client";
import { env } from "@/env";
import { type TTemplateFieldMetaType } from "@/trpc/routers/template-field-router/schema";
import type { TTemplateFieldMetaType } from "@/trpc/routers/template-field-router/schema";
declare global {
// biome-ignore lint/style/noNamespace: <explanation>
namespace PrismaJson {
type TemplateFieldMeta = TTemplateFieldMetaType;
}
@@ -17,8 +16,7 @@ const globalForPrisma = globalThis as unknown as {
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log:
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
log: env.LOGS ? ["query", "error", "warn"] : ["error"],
});
if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;
+3 -3
View File
@@ -7,10 +7,11 @@ import {
} from "@trpc/client";
import { callProcedure } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { type TRPCErrorResponse } from "@trpc/server/rpc";
import type { TRPCErrorResponse } from "@trpc/server/rpc";
import { cookies } from "next/headers";
import { cache } from "react";
import { env } from "@/env";
import { type AppRouter, appRouter } from "@/trpc/api/root";
import { createTRPCContext } from "@/trpc/api/trpc";
import { transformer } from "./shared";
@@ -33,8 +34,7 @@ export const api = createTRPCProxyClient<AppRouter>({
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
env.LOGS || (op.direction === "down" && op.result instanceof Error),
}),
/**
* Custom RSC link that lets us invoke procedures without using http requests. Since Server