166 lines
3.9 KiB
TypeScript
166 lines
3.9 KiB
TypeScript
import * as trpcExpress from "@trpc/server/adapters/express"
|
|
import path from "path"
|
|
import express from "express"
|
|
import cors from "cors"
|
|
import { prisma } from "./utils/prisma"
|
|
import swaggerUi from "swagger-ui-express"
|
|
|
|
import { createContext, router } from "./trpc"
|
|
import { userRouter } from "./user/router"
|
|
import { listRouter } from "./list/router"
|
|
import { organizationRouter } from "./organization/router"
|
|
import { subscriberRouter } from "./subscriber/router"
|
|
import { templateRouter } from "./template/router"
|
|
import { campaignRouter } from "./campaign/router"
|
|
import { messageRouter } from "./message/router"
|
|
import { settingsRouter } from "./settings/router"
|
|
import swaggerSpec from "./swagger"
|
|
import { apiRouter } from "./api/server"
|
|
import { dashboardRouter } from "./dashboard/router"
|
|
import { statsRouter } from "./stats/router"
|
|
import { ONE_PX_PNG } from "./constants"
|
|
|
|
const appRouter = router({
|
|
user: userRouter,
|
|
list: listRouter,
|
|
organization: organizationRouter,
|
|
subscriber: subscriberRouter,
|
|
template: templateRouter,
|
|
campaign: campaignRouter,
|
|
message: messageRouter,
|
|
settings: settingsRouter,
|
|
dashboard: dashboardRouter,
|
|
stats: statsRouter,
|
|
})
|
|
|
|
export type AppRouter = typeof appRouter
|
|
|
|
export const app = express()
|
|
|
|
app.use(
|
|
cors({
|
|
origin: ["http://localhost:3000", "http://localhost:4173"],
|
|
})
|
|
)
|
|
app.use(express.json())
|
|
|
|
app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec))
|
|
|
|
app.get("/t/:id", async (req, res) => {
|
|
try {
|
|
const { id } = req.params
|
|
const subscriberId = req.query.sid
|
|
|
|
const trackedLink = await prisma.trackedLink.findUnique({
|
|
where: { id },
|
|
})
|
|
|
|
if (!trackedLink) {
|
|
res.status(404).send("Link not found")
|
|
return
|
|
}
|
|
|
|
res.redirect(trackedLink.url)
|
|
|
|
if (subscriberId && typeof subscriberId === "string") {
|
|
await prisma
|
|
.$transaction(async (tx) => {
|
|
// add a new click
|
|
await tx.click.create({
|
|
data: {
|
|
subscriberId,
|
|
trackedLinkId: trackedLink.id,
|
|
},
|
|
})
|
|
|
|
if (!trackedLink.campaignId) return
|
|
|
|
const message = await tx.message.findFirst({
|
|
where: {
|
|
campaignId: trackedLink.campaignId,
|
|
subscriberId,
|
|
status: {
|
|
not: "CLICKED",
|
|
},
|
|
},
|
|
})
|
|
|
|
if (!message) return
|
|
|
|
await tx.message.update({
|
|
where: {
|
|
id: message.id,
|
|
},
|
|
data: {
|
|
status: "CLICKED",
|
|
},
|
|
})
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error updating message status", error)
|
|
})
|
|
}
|
|
} catch (error) {
|
|
res.status(404).send("Link not found")
|
|
}
|
|
})
|
|
|
|
app.get("/img/:id/img.png", async (req, res) => {
|
|
// Send pixel immediately
|
|
const pixel = Buffer.from(ONE_PX_PNG, "base64")
|
|
res.setHeader("Content-Type", "image/png")
|
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
res.setHeader("Pragma", "no-cache")
|
|
res.setHeader("Expires", "0")
|
|
res.end(pixel)
|
|
|
|
const id = req.params.id
|
|
|
|
try {
|
|
await prisma.$transaction(async (tx) => {
|
|
const message = await tx.message.findUnique({
|
|
where: {
|
|
id,
|
|
Campaign: {
|
|
openTracking: true,
|
|
},
|
|
},
|
|
})
|
|
|
|
if (!message) {
|
|
return
|
|
}
|
|
|
|
if (message.status !== "SENT") return
|
|
|
|
await tx.message.update({
|
|
where: { id },
|
|
data: {
|
|
status: "OPENED",
|
|
},
|
|
})
|
|
})
|
|
} catch (error) {
|
|
console.error("Error updating message status", error)
|
|
}
|
|
})
|
|
|
|
app.use("/api", apiRouter)
|
|
|
|
app.use(
|
|
"/trpc",
|
|
trpcExpress.createExpressMiddleware({
|
|
router: appRouter,
|
|
createContext,
|
|
})
|
|
)
|
|
|
|
const staticPath = path.join(__dirname, "..", "..", "web", "dist")
|
|
|
|
// serve SPA content
|
|
app.use(express.static(staticPath))
|
|
|
|
app.get("*", (_, res) => {
|
|
res.sendFile(path.join(staticPath, "index.html"))
|
|
})
|