File size: 3,087 Bytes
7bcfa1f
2b62b0c
 
 
 
 
 
cfc59e4
 
 
2b62b0c
f90228b
cfc59e4
 
bede2fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cfc59e4
 
 
 
 
 
 
 
 
 
83e82db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cfc59e4
 
 
 
 
 
 
 
 
 
 
 
a5bd496
7bcfa1f
a5bd496
2a80c9b
a5bd496
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cfc59e4
 
 
 
26ef438
 
 
 
 
7bcfa1f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import { trpcServer } from "@hono/trpc-server";
import { createContext } from "@pram/api/context";
import { appRouter } from "@pram/api/routers/index";
import { checkRateLimitAllowed } from "@pram/api/lib/rate-limit";
import { auth } from "@pram/auth";
import { env } from "@pram/env/server";
import { withRequestId } from "@pram/api/logger";
import { Hono } from "hono";
import { cors } from "hono/cors";

import "@pram/api/queue";

const app = new Hono();

app.use("/*", async (c, next) => {
  const reqId = c.req.header("x-request-id") ?? crypto.randomUUID();
  const reqLogger = withRequestId(reqId);
  const start = Date.now();

  c.res.headers.set("x-request-id", reqId);
  reqLogger.info(`${c.req.method} ${c.req.path}`);

  await next();

  const duration = Date.now() - start;
  reqLogger.info(`${c.req.method} ${c.req.path} ${c.res.status}`, { durationMs: duration });
});

app.use("/*", async (c, next) => {
  await next();
  c.res.headers.set("X-Content-Type-Options", "nosniff");
  c.res.headers.set("X-Frame-Options", "DENY");
  c.res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
  c.res.headers.set("X-XSS-Protection", "0");
  c.res.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
});

app.use(
  "/*",
  cors({
    origin: env.CORS_ORIGIN,
    allowMethods: ["GET", "POST", "OPTIONS"],
    allowHeaders: ["Content-Type", "Authorization"],
    credentials: true,
  }),
);

// Rate limit sign-up attempts per IP to mitigate mass account creation
app.use("/api/auth/sign-up/email", async (c, next) => {
  if (c.req.method !== "POST") return next();
  const cfIp = c.req.header("cf-connecting-ip");
  const forwarded = c.req.header("x-forwarded-for");
  const realIp = c.req.header("x-real-ip");
  const firstForwardedIp = forwarded ? forwarded.split(",")[0]?.trim() : undefined;
  const ip = cfIp ?? firstForwardedIp ?? realIp ?? "unknown";
  const allowed = await checkRateLimitAllowed({ key: `signup:ip:${ip}`, limit: 5, windowMs: 3_600_000 });
  if (!allowed) {
    return c.json({ error: "Too many sign-up attempts. Please try again later." }, 429);
  }
  return next();
});

app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw));

app.use(
  "/trpc/*",
  trpcServer({
    router: appRouter,
    createContext: (_opts, context) => {
      return createContext({ context });
    },
  }),
);

app.get("/sitemap.xml", (c) => {
  const baseUrl = "https://pram-app.vercel.app";
  const urls = [
    { loc: `${baseUrl}/`, changefreq: "weekly", priority: "1.0" },
  ];
  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls
  .map(
    (url) => `  <url>
    <loc>${url.loc}</loc>
    <changefreq>${url.changefreq}</changefreq>
    <priority>${url.priority}</priority>
  </url>`,
  )
  .join("\n")}
</urlset>`;
  return c.text(sitemap, 200, { "Content-Type": "application/xml" });
});

app.get("/", (c) => {
  return c.text("OK");
});

export default {
  port: process.env.PORT || 3000,
  hostname: "0.0.0.0",
  fetch: app.fetch,
};