This commit is contained in:
 Илья Шалонин 2025-10-29 13:43:43 +05:00
commit 8538ab969a
434 changed files with 44051 additions and 0 deletions

80
packages/api/src/app.ts Normal file
View file

@ -0,0 +1,80 @@
import { auth } from "@repo/auth";
import { config } from "@repo/config";
import { getBaseUrl } from "@repo/utils";
import { Scalar } from "@scalar/hono-api-reference";
import { Hono } from "hono";
import { openAPISpecs } from "hono-openapi";
import {} from "openapi-merge";
import {} from "openapi-merge";
import { mergeOpenApiSchemas } from "./lib/openapi-schema";
import { corsMiddleware } from "./middleware/cors";
import { loggerMiddleware } from "./middleware/logger";
import { adminRouter } from "./routes/admin/router";
import { aiRouter } from "./routes/ai";
import { authRouter } from "./routes/auth";
import { contactRouter } from "./routes/contact/router";
import { healthRouter } from "./routes/health";
import { newsletterRouter } from "./routes/newsletter";
import { organizationsRouter } from "./routes/organizations/router";
import { paymentsRouter } from "./routes/payments/router";
import { uploadsRouter } from "./routes/uploads";
import { webhooksRouter } from "./routes/webhooks";
export const app = new Hono().basePath("/api");
app.use(loggerMiddleware);
app.use(corsMiddleware);
const appRouter = app
.route("/", authRouter)
.route("/", webhooksRouter)
.route("/", aiRouter)
.route("/", uploadsRouter)
.route("/", paymentsRouter)
.route("/", contactRouter)
.route("/", newsletterRouter)
.route("/", organizationsRouter)
.route("/", adminRouter)
.route("/", healthRouter);
app.get(
"/app-openapi",
openAPISpecs(app, {
documentation: {
info: {
title: `${config.appName} API`,
version: "1.0.0",
},
servers: [
{
url: getBaseUrl(),
description: "API server",
},
],
},
}),
);
app.get("/openapi", async (c) => {
const authSchema = await auth.api.generateOpenAPISchema();
const appSchema = await (
app.request("/api/app-openapi") as Promise<Response>
).then((res) => res.json());
const mergedSchema = mergeOpenApiSchemas({
appSchema,
authSchema: authSchema as any,
});
return c.json(mergedSchema);
});
app.get(
"/docs",
Scalar({
theme: "saturn",
url: "/api/openapi",
}),
);
export type AppRouter = typeof appRouter;

View file

@ -0,0 +1,45 @@
import { type MergeInput, isErrorResult, merge } from "openapi-merge";
export function mergeOpenApiSchemas({
appSchema,
authSchema,
}: {
appSchema: MergeInput[number]["oas"];
authSchema: MergeInput[number]["oas"];
}) {
const mergedSchema = merge([
{
oas: appSchema,
},
{
oas: authSchema,
pathModification: {
prepend: "/api/auth",
},
},
]);
if (isErrorResult(mergedSchema)) {
return {};
}
const output = mergedSchema.output;
Object.entries(output.paths).forEach(([path, pathItem]) => {
if (path.startsWith("/api/auth")) {
const methods = ["post", "get", "put", "delete", "patch"];
methods.forEach((method) => {
const methodItem = pathItem[method as keyof typeof pathItem];
if (
methodItem &&
typeof methodItem === "object" &&
!Array.isArray(methodItem)
) {
methodItem.tags = ["Auth"];
}
});
}
});
return output;
}

View file

@ -0,0 +1,26 @@
import { type Session, auth } from "@repo/auth";
import { createMiddleware } from "hono/factory";
export const adminMiddleware = createMiddleware<{
Variables: {
session: Session["session"];
user: Session["user"];
};
}>(async (c, next) => {
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
return c.json({ error: "Unauthorized" }, 401);
}
if (session.user.role !== "admin") {
return c.json({ error: "Forbidden" }, 403);
}
c.set("session", session.session);
c.set("user", session.user);
await next();
});

View file

@ -0,0 +1,22 @@
import { type Session, auth } from "@repo/auth";
import { createMiddleware } from "hono/factory";
export const authMiddleware = createMiddleware<{
Variables: {
session: Session["session"];
user: Session["user"];
};
}>(async (c, next) => {
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
return c.json({ error: "Unauthorized" }, 401);
}
c.set("session", session.session);
c.set("user", session.user);
await next();
});

View file

@ -0,0 +1,11 @@
import { getBaseUrl } from "@repo/utils";
import { cors } from "hono/cors";
export const corsMiddleware = cors({
origin: getBaseUrl(),
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
});

View file

@ -0,0 +1,18 @@
import { config } from "@repo/config";
import type { Locale } from "@repo/i18n";
import { getCookie } from "hono/cookie";
import { createMiddleware } from "hono/factory";
export const localeMiddleware = createMiddleware<{
Variables: {
locale: Locale;
};
}>(async (c, next) => {
const locale =
(getCookie(c, config.i18n.localeCookieName) as Locale) ??
config.i18n.defaultLocale;
c.set("locale", locale);
await next();
});

View file

@ -0,0 +1,6 @@
import { logger } from "@repo/logs";
import { logger as honoLogger } from "hono/logger";
export const loggerMiddleware = honoLogger((message, ...rest) => {
logger.log(message, ...rest);
});

View file

@ -0,0 +1,54 @@
import {
countAllOrganizations,
getOrganizationById,
getOrganizations,
} from "@repo/database";
import { Hono } from "hono";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import { HTTPException } from "hono/http-exception";
import { z } from "zod";
import { adminMiddleware } from "../../middleware/admin";
export const organizationRouter = new Hono()
.basePath("/organizations")
.use(adminMiddleware)
.get(
"/",
validator(
"query",
z.object({
query: z.string().optional(),
limit: z.string().optional().default("10").transform(Number),
offset: z.string().optional().default("0").transform(Number),
}),
),
describeRoute({
summary: "Get all organizations",
tags: ["Administration"],
}),
async (c) => {
const { query, limit, offset } = c.req.valid("query");
const organizations = await getOrganizations({
limit,
offset,
query,
});
const total = await countAllOrganizations();
return c.json({ organizations, total });
},
)
.get("/:id", async (c) => {
const id = c.req.param("id");
const organization = await getOrganizationById(id);
if (!organization) {
throw new HTTPException(404);
}
return c.json(organization);
});

View file

@ -0,0 +1,8 @@
import { Hono } from "hono";
import { organizationRouter } from "./organizations";
import { userRouter } from "./users";
export const adminRouter = new Hono()
.basePath("/admin")
.route("/", organizationRouter)
.route("/", userRouter);

View file

@ -0,0 +1,38 @@
import { countAllUsers, getUsers } from "@repo/database";
import { Hono } from "hono";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import { z } from "zod";
import { adminMiddleware } from "../../middleware/admin";
export const userRouter = new Hono()
.basePath("/users")
.use(adminMiddleware)
.get(
"/",
validator(
"query",
z.object({
query: z.string().optional(),
limit: z.string().optional().default("10").transform(Number),
offset: z.string().optional().default("0").transform(Number),
}),
),
describeRoute({
summary: "Get all users",
tags: ["Administration"],
}),
async (c) => {
const { query, limit, offset } = c.req.valid("query");
const users = await getUsers({
limit,
offset,
query,
});
const total = await countAllUsers();
return c.json({ users, total });
},
);

View file

@ -0,0 +1,303 @@
import { streamText, textModel } from "@repo/ai";
import {
AiChatSchema,
createAiChat,
deleteAiChat,
getAiChatById,
getAiChatsByOrganizationId,
getAiChatsByUserId,
updateAiChat,
} from "@repo/database";
import { Hono } from "hono";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { HTTPException } from "hono/http-exception";
import { z } from "zod";
import { authMiddleware } from "../middleware/auth";
import { verifyOrganizationMembership } from "./organizations/lib/membership";
const MessageSchema = z.object({
role: z.enum(["user", "assistant"]),
content: z.string(),
});
const ChatSchema = AiChatSchema.extend({
messages: z.array(MessageSchema),
});
export const aiRouter = new Hono()
.basePath("/ai")
.use(authMiddleware)
.get(
"/chats",
describeRoute({
tags: ["AI"],
summary: "Get chats",
description: "Get all chats for current user or organization",
responses: {
200: {
description: "Chats",
content: {
"application/json": {
schema: resolver(
z.object({ chats: z.array(ChatSchema) }),
),
},
},
},
},
}),
validator(
"query",
z.object({ organizationId: z.string().optional() }).optional(),
),
async (c) => {
const query = c.req.valid("query");
const chats = await (query?.organizationId
? getAiChatsByOrganizationId({
limit: 10,
offset: 0,
organizationId: query?.organizationId,
})
: getAiChatsByUserId({
limit: 10,
offset: 0,
userId: c.get("user").id,
}));
return c.json({ chats });
},
)
.get(
"/chats/:id",
describeRoute({
tags: ["AI"],
summary: "Get chat",
description: "Get a chat by id",
responses: {
200: {
description: "Chat",
content: {
"application/json": {
schema: resolver(z.object({ chat: ChatSchema })),
},
},
},
},
}),
async (c) => {
const { id } = c.req.param();
const chat = await getAiChatById(id);
if (!chat) {
throw new HTTPException(404, { message: "Chat not found" });
}
if (chat.organizationId) {
await verifyOrganizationMembership(
chat.organizationId,
c.get("user").id,
);
} else if (chat.userId !== c.get("user").id) {
throw new HTTPException(403, { message: "Forbidden" });
}
return c.json({ chat });
},
)
.post(
"/chats",
describeRoute({
tags: ["AI"],
summary: "Create chat",
description: "Create a new chat",
responses: {
200: {
description: "Chat",
content: {
"application/json": {
schema: resolver(z.object({ chat: ChatSchema })),
},
},
},
},
}),
validator(
"json",
z.object({
title: z.string().optional(),
organizationId: z.string().optional(),
}),
),
async (c) => {
const { title, organizationId } = c.req.valid("json");
const user = c.get("user");
if (organizationId) {
await verifyOrganizationMembership(organizationId, user.id);
}
const chat = await createAiChat({
title: title,
organizationId,
userId: user.id,
});
if (!chat) {
throw new HTTPException(500, {
message: "Failed to create chat",
});
}
return c.json({ chat });
},
)
.put(
"/chats/:id",
describeRoute({
tags: ["AI"],
summary: "Update chat",
description: "Update a chat by id",
responses: {
200: {
description: "Chat",
content: {
"application/json": {
schema: resolver(z.object({ chat: ChatSchema })),
},
},
},
},
}),
validator("json", z.object({ title: z.string().optional() })),
async (c) => {
const { id } = c.req.param();
const { title } = c.req.valid("json");
const user = c.get("user");
const chat = await getAiChatById(id);
if (!chat) {
throw new HTTPException(404, { message: "Chat not found" });
}
if (chat.organizationId) {
await verifyOrganizationMembership(
chat.organizationId,
user.id,
);
} else if (chat.userId !== c.get("user").id) {
throw new HTTPException(403, { message: "Forbidden" });
}
const updatedChat = await updateAiChat({
id,
title,
});
return c.json({ chat: updatedChat });
},
)
.delete(
"/chats/:id",
describeRoute({
tags: ["AI"],
summary: "Delete chat",
description: "Delete a chat by id",
responses: {
204: {
description: "Chat deleted",
},
},
}),
async (c) => {
const { id } = c.req.param();
const user = c.get("user");
const chat = await getAiChatById(id);
if (!chat) {
throw new HTTPException(404, { message: "Chat not found" });
}
if (chat.organizationId) {
await verifyOrganizationMembership(
chat.organizationId,
user.id,
);
} else if (chat.userId !== c.get("user").id) {
throw new HTTPException(403, { message: "Forbidden" });
}
await deleteAiChat(id);
return c.body(null, 204);
},
)
.post(
"/chats/:id/messages",
describeRoute({
tags: ["AI"],
summary: "Add message to chat",
description:
"Send all messages of the chat to the AI model to get a response",
responses: {
200: {
description:
"Returns a stream of the response from the AI model",
},
},
}),
validator(
"json",
z.object({
messages: z.array(
z.object({
role: z.enum(["user", "assistant"]),
content: z.string(),
}),
),
}),
),
async (c) => {
const { id } = c.req.param();
const { messages } = c.req.valid("json");
const user = c.get("user");
const chat = await getAiChatById(id);
if (!chat) {
throw new HTTPException(404, { message: "Chat not found" });
}
if (chat.organizationId) {
await verifyOrganizationMembership(
chat.organizationId,
user.id,
);
} else if (chat.userId !== c.get("user").id) {
throw new HTTPException(403, { message: "Forbidden" });
}
const response = streamText({
model: textModel,
messages,
async onFinish({ text }) {
await updateAiChat({
id,
messages: [
...messages,
{
role: "assistant",
content: text,
},
],
});
},
});
return response.toDataStreamResponse({
sendUsage: true,
});
},
);

View file

@ -0,0 +1,10 @@
import { auth } from "@repo/auth";
import { Hono } from "hono";
export const authRouter = new Hono()
.get("/auth/**", (c) => {
return auth.handler(c.req.raw);
})
.post("/auth/**", (c) => {
return auth.handler(c.req.raw);
});

View file

@ -0,0 +1,51 @@
import { config } from "@repo/config";
import { logger } from "@repo/logs";
import { sendEmail } from "@repo/mail";
import { Hono } from "hono";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { localeMiddleware } from "../../middleware/locale";
import { contactFormSchema } from "./types";
export const contactRouter = new Hono().basePath("/contact").post(
"/",
localeMiddleware,
validator("form", contactFormSchema),
describeRoute({
tags: ["Contact"],
summary: "Send a message from the contact form",
description: "Send a message with an email and name",
responses: {
204: {
description: "Message sent",
},
400: {
description: "Could not send message",
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
},
},
}),
async (c) => {
const { email, name, message } = c.req.valid("form");
const locale = c.get("locale");
try {
await sendEmail({
to: config.contactForm.to,
locale,
subject: config.contactForm.subject,
text: `Name: ${name}\n\nEmail: ${email}\n\nMessage: ${message}`,
});
return c.body(null, 204);
} catch (error) {
logger.error(error);
return c.json({ error: "Could not send email" }, 500);
}
},
);

View file

@ -0,0 +1,9 @@
import { z } from "zod";
export const contactFormSchema = z.object({
email: z.string().email(),
name: z.string().min(3),
message: z.string().min(10),
});
export type ContactFormValues = z.infer<typeof contactFormSchema>;

View file

@ -0,0 +1,13 @@
import { Hono } from "hono";
import { describeRoute } from "hono-openapi";
export const healthRouter = new Hono().get(
"/health",
describeRoute({
tags: ["Health"],
summary: "Health check",
description: "Returns 200 if the server is healthy",
responses: { 200: { description: "OK" } },
}),
() => new Response("OK"),
);

View file

@ -0,0 +1,54 @@
import { logger } from "@repo/logs";
import { sendEmail } from "@repo/mail";
import { Hono } from "hono";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { localeMiddleware } from "../middleware/locale";
export const newsletterRouter = new Hono().basePath("/newsletter").post(
"/signup",
localeMiddleware,
validator(
"form",
z.object({
email: z.string().email(),
}),
),
describeRoute({
tags: ["Newsletter"],
summary: "Sign up for the newsletter",
description: "Takes an email and sends a confirmation email",
responses: {
204: {
description: "Email sent",
},
500: {
description: "Could not send email",
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
},
},
}),
async (c) => {
const { email } = c.req.valid("form");
const locale = c.get("locale");
try {
await sendEmail({
to: email,
locale,
templateId: "newsletterSignup",
context: {},
});
return c.body(null, 204);
} catch (error) {
logger.error(error);
return c.json({ error: "Could not send email" }, 500);
}
},
);

View file

@ -0,0 +1,20 @@
import { getOrganizationMembership } from "@repo/database";
import { HTTPException } from "hono/http-exception";
export async function verifyOrganizationMembership(
organizationId: string,
userId: string,
) {
const membership = await getOrganizationMembership(organizationId, userId);
if (!membership) {
throw new HTTPException(404, {
message: "User is not a member of this organization",
});
}
return {
organization: membership.organization,
role: membership.role,
};
}

View file

@ -0,0 +1,55 @@
import { getOrganizationBySlug } from "@repo/database";
import slugify from "@sindresorhus/slugify";
import { Hono } from "hono";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import { nanoid } from "nanoid";
import { z } from "zod";
export const organizationsRouter = new Hono().basePath("/organizations").get(
"/generate-slug",
validator(
"query",
z.object({
name: z.string(),
}),
),
describeRoute({
summary: "Generate a slug for an organization",
tags: ["Organizations"],
}),
async (c) => {
const { name } = c.req.valid("query");
const baseSlug = slugify(name, {
lowercase: true,
});
let slug = baseSlug;
let hasAvailableSlug = false;
for (let i = 0; i < 3; i++) {
const existing = await getOrganizationBySlug(slug);
if (!existing) {
hasAvailableSlug = true;
break;
}
slug = `${baseSlug}-${nanoid(5)}`;
}
if (!hasAvailableSlug) {
return c.json(
{
error: "No available slug found",
},
400,
);
}
return c.json({
slug,
});
},
);

View file

@ -0,0 +1,21 @@
import {
getPurchasesByOrganizationId,
getPurchasesByUserId,
} from "@repo/database";
export const getPurchases = async (
props: { organizationId: string } | { userId: string },
) => {
if ("organizationId" in props) {
const { organizationId } = props;
const purchases = await getPurchasesByOrganizationId(organizationId);
return purchases;
}
const { userId } = props;
const purchases = await getPurchasesByUserId(userId);
return purchases;
};

View file

@ -0,0 +1,217 @@
import { type Config, config } from "@repo/config";
import { getOrganizationMembership } from "@repo/database";
import {
PurchaseSchema,
getOrganizationById,
getPurchaseById,
} from "@repo/database";
import { logger } from "@repo/logs";
import {
createCheckoutLink,
createCustomerPortalLink,
getCustomerIdFromEntity,
} from "@repo/payments";
import { Hono } from "hono";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { HTTPException } from "hono/http-exception";
import { z } from "zod";
import { authMiddleware } from "../../middleware/auth";
import { getPurchases } from "./lib/purchases";
const plans = config.payments.plans as Config["payments"]["plans"];
export const paymentsRouter = new Hono()
.basePath("/payments")
.get(
"/purchases",
authMiddleware,
validator(
"query",
z.object({
organizationId: z.string().optional(),
}),
),
describeRoute({
tags: ["Payments"],
summary: "Get purchases",
description:
"Get all purchases of the current user or the provided organization",
responses: {
200: {
description: "Purchases",
content: {
"application/json": {
schema: resolver(z.array(PurchaseSchema)),
},
},
},
},
}),
async (c) => {
const { organizationId } = c.req.valid("query");
const user = c.get("user");
const purchases = await getPurchases(
organizationId
? {
organizationId,
}
: { userId: user.id },
);
return c.json(purchases);
},
)
.post(
"/create-checkout-link",
authMiddleware,
validator(
"query",
z.object({
type: z.enum(["one-time", "subscription"]),
productId: z.string(),
redirectUrl: z.string().optional(),
organizationId: z.string().optional(),
}),
),
describeRoute({
tags: ["Payments"],
summary: "Create a checkout link",
description:
"Creates a checkout link for a one-time or subscription product",
responses: {
200: {
description: "Checkout link",
},
},
}),
async (c) => {
const { productId, redirectUrl, type, organizationId } =
c.req.valid("query");
const user = c.get("user");
const customerId = await getCustomerIdFromEntity(
organizationId
? {
organizationId,
}
: {
userId: user.id,
},
);
const plan = Object.entries(plans).find(([_planId, plan]) =>
plan.prices?.find((price) => price.productId === productId),
);
const price = plan?.[1].prices?.find(
(price) => price.productId === productId,
);
const trialPeriodDays =
price && "trialPeriodDays" in price
? price.trialPeriodDays
: undefined;
const organization = organizationId
? await getOrganizationById(organizationId)
: undefined;
if (organization === null) {
throw new HTTPException(404);
}
const seats =
organization && price && "seatBased" in price && price.seatBased
? organization.members.length
: undefined;
try {
const checkoutLink = await createCheckoutLink({
type,
productId,
email: user.email,
name: user.name ?? "",
redirectUrl,
...(organizationId
? { organizationId }
: { userId: user.id }),
trialPeriodDays,
seats,
customerId: customerId ?? undefined,
});
if (!checkoutLink) {
throw new HTTPException(500);
}
return c.json({ checkoutLink });
} catch (e) {
logger.error(e);
throw new HTTPException(500);
}
},
)
.post(
"/create-customer-portal-link",
authMiddleware,
validator(
"query",
z.object({
purchaseId: z.string(),
redirectUrl: z.string().optional(),
}),
),
describeRoute({
tags: ["Payments"],
summary: "Create a customer portal link",
description:
"Creates a customer portal link for the customer or team. If a purchase is provided, the link will be created for the customer of the purchase.",
responses: {
200: {
description: "Customer portal link",
},
},
}),
async (c) => {
const { purchaseId, redirectUrl } = c.req.valid("query");
const user = c.get("user");
const purchase = await getPurchaseById(purchaseId);
if (!purchase) {
throw new HTTPException(403);
}
if (purchase.organizationId) {
const userOrganizationMembership =
await getOrganizationMembership(
purchase.organizationId,
user.id,
);
if (userOrganizationMembership?.role !== "owner") {
throw new HTTPException(403);
}
}
if (purchase.userId && purchase.userId !== user.id) {
throw new HTTPException(403);
}
try {
const customerPortalLink = await createCustomerPortalLink({
subscriptionId: purchase.subscriptionId ?? undefined,
customerId: purchase.customerId,
redirectUrl,
});
if (!customerPortalLink) {
throw new HTTPException(500);
}
return c.json({ customerPortalLink });
} catch (e) {
logger.error("Could not create customer portal link", e);
throw new HTTPException(500);
}
},
);

View file

@ -0,0 +1,22 @@
import { z } from "zod";
export const SubscriptionPlanVariantModel = z.object({
id: z.string(),
price: z.number(),
currency: z.string(),
interval: z.string(),
interval_count: z.number(),
});
export const SubscriptionPlanModel = z.object({
id: z.string(),
name: z.string(),
description: z.string().nullable().optional(),
storeId: z.string().nullable().optional(),
variants: z.array(SubscriptionPlanVariantModel),
});
export type SubscriptionPlan = z.infer<typeof SubscriptionPlanModel>;
export type SubscriptionPlanVariant = z.infer<
typeof SubscriptionPlanVariantModel
>;

View file

@ -0,0 +1,50 @@
import { config } from "@repo/config";
import { getSignedUploadUrl } from "@repo/storage";
import { Hono } from "hono";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { HTTPException } from "hono/http-exception";
import { z } from "zod";
import { authMiddleware } from "../middleware/auth";
export const uploadsRouter = new Hono().basePath("/uploads").post(
"/signed-upload-url",
authMiddleware,
validator(
"query",
z.object({
bucket: z.string().min(1),
path: z.string().min(1),
}),
),
describeRoute({
tags: ["Uploads"],
summary: "Get a signed upload url",
description: "Get a signed upload url for a given bucket and path",
responses: {
200: {
description: "Returns a signed upload url",
content: {
"application/json": {
schema: resolver(z.object({ signedUrl: z.string() })),
},
},
},
403: {
description: "Not allowed",
},
},
}),
async (c) => {
const { bucket, path } = c.req.valid("query");
// ATTENTION: be careful with how you give access to write to the storage
// always check if the user has the right to write to the desired bucket before giving them a signed url
if (bucket === config.storage.bucketNames.avatars) {
const signedUrl = await getSignedUploadUrl(path, { bucket });
return c.json({ signedUrl });
}
throw new HTTPException(403);
},
);

View file

@ -0,0 +1,14 @@
import { webhookHandler as paymentsWebhookHandler } from "@repo/payments";
import { Hono } from "hono";
import { describeRoute } from "hono-openapi";
export const webhooksRouter = new Hono().post(
"/webhooks/payments",
describeRoute({
tags: ["Webhooks"],
summary: "Handle payments webhook",
}),
(c) => {
return paymentsWebhookHandler(c.req.raw);
},
);