mirror of
https://github.com/hanzoai/captable.git
synced 2026-10-16 14:21:29 +00:00
Merge branch 'main' into trpc-sentry-middleware
This commit is contained in:
+1
-1
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user