Skip to content

Commit 5638370

Browse files
authored
Merge pull request #86 from apsinghdev/migrate/backend
migrate backend to trpc
2 parents 33b5c65 + 65ab676 commit 5638370

35 files changed

+1641
-252
lines changed

.dockerignore

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Dependencies
2+
node_modules
3+
**/node_modules
4+
5+
# Build outputs
6+
dist
7+
**/dist
8+
.next
9+
**/out
10+
11+
# Testing
12+
coverage
13+
**/.coverage
14+
15+
# Environment files
16+
.env
17+
.env.*
18+
!.env.example
19+
20+
# Git
21+
.git
22+
.gitignore
23+
**/.git
24+
25+
# IDE
26+
.vscode
27+
.idea
28+
*.swp
29+
*.swo
30+
*~
31+
32+
# OS
33+
.DS_Store
34+
Thumbs.db
35+
36+
# Logs
37+
logs
38+
*.log
39+
npm-debug.log*
40+
yarn-debug.log*
41+
yarn-error.log*
42+
pnpm-debug.log*
43+
44+
# Misc
45+
README.md
46+
**/README.md
47+
LICENSE
48+
*.md
49+
!apps/api/src/**/*.md
50+
51+
# Apps not needed for API
52+
apps/web
53+
apps/backend
54+
apps/docs
55+
56+
# Turbo
57+
.turbo
58+
**/.turbo
59+
turbo.json
60+

Dockerfile

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
FROM node:20 AS builder
2+
3+
WORKDIR /app
4+
5+
# Install pnpm
6+
RUN npm install -g pnpm
7+
8+
# Copy workspace configuration files
9+
COPY pnpm-workspace.yaml ./
10+
COPY package.json ./
11+
COPY pnpm-lock.yaml* ./
12+
13+
# Copy package.json files for all workspaces (for better layer caching)
14+
COPY apps/api/package.json ./apps/api/
15+
COPY packages/shared/package.json ./packages/shared/
16+
17+
# Install dependencies for entire workspace
18+
RUN pnpm install
19+
20+
# Copy shared package source (types directory contains the actual source files)
21+
COPY packages/shared/types ./packages/shared/types
22+
COPY packages/shared/tsconfig.json ./packages/shared/tsconfig.json
23+
24+
# Copy API source and config
25+
COPY apps/api/src ./apps/api/src
26+
COPY apps/api/tsconfig.json ./apps/api/
27+
COPY apps/api/prisma ./apps/api/prisma
28+
29+
# Build shared package first
30+
WORKDIR /app/packages/shared
31+
RUN pnpm run build
32+
33+
# Generate Prisma client
34+
WORKDIR /app/apps/api
35+
RUN pnpm exec prisma generate
36+
37+
# Build API
38+
RUN pnpm run build
39+
40+
# Production stage
41+
FROM node:20-slim
42+
43+
WORKDIR /app
44+
45+
# Install OpenSSL and other dependencies required by Prisma
46+
RUN apt-get update -y && \
47+
apt-get install -y openssl ca-certificates && \
48+
rm -rf /var/lib/apt/lists/*
49+
50+
# Install pnpm
51+
RUN npm install -g pnpm
52+
53+
# Copy workspace configuration
54+
COPY pnpm-workspace.yaml ./
55+
COPY package.json ./
56+
COPY pnpm-lock.yaml* ./
57+
58+
# Copy package.json files
59+
COPY apps/api/package.json ./apps/api/
60+
COPY packages/shared/package.json ./packages/shared/
61+
62+
# Copy built artifacts from builder
63+
COPY --from=builder /app/apps/api/dist ./apps/api/dist
64+
COPY --from=builder /app/packages/shared/dist ./packages/shared/dist
65+
COPY --from=builder /app/apps/api/prisma ./apps/api/prisma
66+
67+
# Install all dependencies (needed for Prisma generation)
68+
RUN pnpm install
69+
70+
# Generate Prisma client in production stage
71+
WORKDIR /app/apps/api
72+
RUN pnpm exec prisma generate
73+
74+
# Remove devDependencies after Prisma generation
75+
RUN pnpm install --prod
76+
77+
WORKDIR /app/apps/api
78+
79+
EXPOSE 4000
80+
81+
CMD ["node", "dist/index.js"]

apps/api/package.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "api",
3+
"version": "1.0.0",
4+
"description": "",
5+
"type": "module",
6+
"main": "index.ts",
7+
"scripts": {
8+
"test": "echo \"Error: no test specified\" && exit 1",
9+
"dev": "tsx src/index.ts",
10+
"build": "prisma generate && tsc",
11+
"postinstall": "[ -f prisma/schema.prisma ] && prisma generate || true"
12+
},
13+
"keywords": [],
14+
"author": "Ajeet Pratpa Singh",
15+
"license": "ISC",
16+
"packageManager": "pnpm@10.11.0",
17+
"devDependencies": {
18+
"@types/cors": "^2.8.19",
19+
"@types/express": "^4.17.21",
20+
"@types/jsonwebtoken": "^9.0.7",
21+
"@types/node": "^24.5.1",
22+
"prisma": "^5.22.0",
23+
"tsx": "^4.20.3",
24+
"typescript": "^5.9.2"
25+
},
26+
"dependencies": {
27+
"@octokit/graphql": "^9.0.1",
28+
"@opensox/shared": "workspace:*",
29+
"@prisma/client": "^5.22.0",
30+
"@trpc/server": "^11.5.1",
31+
"cors": "^2.8.5",
32+
"dotenv": "^16.5.0",
33+
"express": "^4.21.2",
34+
"express-rate-limit": "^7.5.0",
35+
"helmet": "^7.2.0",
36+
"jsonwebtoken": "^9.0.2",
37+
"zod": "^4.1.9"
38+
}
39+
}

apps/api/prisma/schema.prisma

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
generator client {
2+
provider = "prisma-client-js"
3+
}
4+
5+
datasource db {
6+
provider = "postgresql"
7+
url = env("DATABASE_URL")
8+
}
9+
10+
model QueryCount {
11+
id Int @id @default(1)
12+
total_queries BigInt
13+
}
14+
15+
model User {
16+
id String @id @default(cuid())
17+
18+
email String @unique
19+
20+
firstName String
21+
22+
authMethod String
23+
24+
createdAt DateTime @default(now())
25+
26+
lastLogin DateTime @updatedAt
27+
28+
accounts Account[]
29+
}
30+
31+
model Account {
32+
id String @id @default(cuid())
33+
34+
userId String // Foreign key to User
35+
36+
type String // "oauth", "email", etc.
37+
38+
provider String // "google", "github", etc.
39+
40+
providerAccountId String // ID from the provider
41+
42+
refresh_token String?
43+
44+
access_token String?
45+
46+
expires_at Int?
47+
48+
token_type String?
49+
50+
scope String?
51+
52+
id_token String?
53+
54+
session_state String?
55+
56+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
57+
58+
@@unique([provider, providerAccountId])
59+
}

apps/api/src/context.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
2+
import prisma from "./prisma.js";
3+
import type { User } from "@prisma/client";
4+
5+
export async function createContext({
6+
req,
7+
res,
8+
}: CreateExpressContextOptions): Promise<{
9+
req: CreateExpressContextOptions["req"];
10+
res: CreateExpressContextOptions["res"];
11+
db: typeof prisma;
12+
ip?: string;
13+
user?: User | null;
14+
}> {
15+
const ip = req.ip || req.socket.remoteAddress || "unknown";
16+
17+
return {
18+
req,
19+
res,
20+
db: prisma,
21+
ip,
22+
user: null,
23+
};
24+
}
25+
26+
export type Context = Awaited<ReturnType<typeof createContext>>;

apps/api/src/index.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import dotenv from "dotenv";
2+
import express from "express";
3+
import type { Request, Response } from "express";
4+
import * as trpcExpress from "@trpc/server/adapters/express";
5+
import { appRouter } from "./routers/_app.js";
6+
import { createContext } from "./context.js";
7+
import prismaModule from "./prisma.js";
8+
import cors from "cors";
9+
import type { CorsOptions as CorsOptionsType } from "cors";
10+
import rateLimit from "express-rate-limit";
11+
import helmet from "helmet";
12+
import ipBlocker from "./middleware/ipBlock.js";
13+
14+
dotenv.config();
15+
16+
const app = express();
17+
const PORT = process.env.PORT || 4000;
18+
const CORS_ORIGINS = process.env.CORS_ORIGINS
19+
? process.env.CORS_ORIGINS.split(",")
20+
: ["http://localhost:3000", "http://localhost:5000"];
21+
22+
// Security headers
23+
app.use(helmet());
24+
app.use(
25+
helmet.contentSecurityPolicy({
26+
directives: {
27+
defaultSrc: ["'self'"],
28+
scriptSrc: ["'self'", "'unsafe-inline'"],
29+
styleSrc: ["'self'", "'unsafe-inline'"],
30+
imgSrc: ["'self'", "data:", "https:"],
31+
},
32+
})
33+
);
34+
35+
// Apply IP blocking middleware first
36+
app.use(ipBlocker.middleware);
37+
38+
// Different rate limits for different endpoints
39+
const authLimiter = rateLimit({
40+
windowMs: 15 * 60 * 1000,
41+
max: 5,
42+
message: "Too many login attempts, please try again later",
43+
standardHeaders: true,
44+
legacyHeaders: false,
45+
});
46+
47+
const apiLimiter = rateLimit({
48+
windowMs: 15 * 60 * 1000,
49+
max: 30,
50+
message: "Too many requests from this IP",
51+
standardHeaders: true,
52+
legacyHeaders: false,
53+
});
54+
55+
// Request size limits
56+
app.use(express.json({ limit: "10kb" }));
57+
app.use(express.urlencoded({ limit: "10kb", extended: true }));
58+
59+
// CORS configuration
60+
const corsOptions: CorsOptionsType = {
61+
origin: (origin, callback) => {
62+
if (!origin || CORS_ORIGINS.includes(origin)) {
63+
callback(null, origin);
64+
} else {
65+
callback(new Error("Not allowed by CORS"));
66+
}
67+
},
68+
methods: ["GET", "POST"],
69+
allowedHeaders: ["Content-Type", "Authorization"],
70+
credentials: true,
71+
maxAge: 86400, // 24 hours
72+
};
73+
74+
app.use(cors(corsOptions));
75+
76+
// Blocked IPs endpoint (admin endpoint)
77+
app.get("/admin/blocked-ips", (req: Request, res: Response) => {
78+
const blockedIPs = ipBlocker.getBlockedIPs();
79+
res.json({
80+
blockedIPs: blockedIPs.map((ip) => ({
81+
...ip,
82+
blockedUntil: new Date(ip.blockedUntil).toISOString(),
83+
})),
84+
});
85+
});
86+
87+
// Test endpoint
88+
app.get("/test", apiLimiter, (req: Request, res: Response) => {
89+
res.status(200).json({ status: "ok", message: "Test endpoint is working" });
90+
});
91+
92+
// Connect to database
93+
prismaModule.connectDB();
94+
95+
// Apply rate limiting to tRPC endpoints
96+
app.use("/trpc", apiLimiter);
97+
98+
// tRPC middleware
99+
app.use(
100+
"/trpc",
101+
trpcExpress.createExpressMiddleware({
102+
router: appRouter,
103+
createContext,
104+
})
105+
);
106+
107+
// Global error handling
108+
app.use((err: Error, req: Request, res: Response, next: Function) => {
109+
console.error(err.stack);
110+
res.status(500).json({
111+
error: "Internal Server Error",
112+
message: process.env.NODE_ENV === "development" ? err.message : undefined,
113+
});
114+
});
115+
116+
app.listen(PORT, () => {
117+
console.log(`tRPC server running on http://localhost:${PORT}`);
118+
});

0 commit comments

Comments
 (0)