Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Dependencies
node_modules
**/node_modules

# Build outputs
dist
**/dist
.next
**/out

# Testing
coverage
**/.coverage

# Environment files
.env
.env.*
!.env.example

# Git
.git
.gitignore
**/.git

# IDE
.vscode
.idea
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Misc
README.md
**/README.md
LICENSE
*.md
!apps/api/src/**/*.md

# Apps not needed for API
apps/web
apps/backend
apps/docs

# Turbo
.turbo
**/.turbo
turbo.json

81 changes: 81 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
FROM node:20 AS builder

WORKDIR /app

# Install pnpm
RUN npm install -g pnpm

# Copy workspace configuration files
COPY pnpm-workspace.yaml ./
COPY package.json ./
COPY pnpm-lock.yaml* ./

# Copy package.json files for all workspaces (for better layer caching)
COPY apps/api/package.json ./apps/api/
COPY packages/shared/package.json ./packages/shared/

# Install dependencies for entire workspace
RUN pnpm install

# Copy shared package source (types directory contains the actual source files)
COPY packages/shared/types ./packages/shared/types
COPY packages/shared/tsconfig.json ./packages/shared/tsconfig.json

# Copy API source and config
COPY apps/api/src ./apps/api/src
COPY apps/api/tsconfig.json ./apps/api/
COPY apps/api/prisma ./apps/api/prisma

# Build shared package first
WORKDIR /app/packages/shared
RUN pnpm run build

# Generate Prisma client
WORKDIR /app/apps/api
RUN pnpm exec prisma generate

# Build API
RUN pnpm run build

# Production stage
FROM node:20-slim

WORKDIR /app

# Install OpenSSL and other dependencies required by Prisma
RUN apt-get update -y && \
apt-get install -y openssl ca-certificates && \
rm -rf /var/lib/apt/lists/*

# Install pnpm
RUN npm install -g pnpm

# Copy workspace configuration
COPY pnpm-workspace.yaml ./
COPY package.json ./
COPY pnpm-lock.yaml* ./

# Copy package.json files
COPY apps/api/package.json ./apps/api/
COPY packages/shared/package.json ./packages/shared/

# Copy built artifacts from builder
COPY --from=builder /app/apps/api/dist ./apps/api/dist
COPY --from=builder /app/packages/shared/dist ./packages/shared/dist
COPY --from=builder /app/apps/api/prisma ./apps/api/prisma

# Install all dependencies (needed for Prisma generation)
RUN pnpm install

# Generate Prisma client in production stage
WORKDIR /app/apps/api
RUN pnpm exec prisma generate

# Remove devDependencies after Prisma generation
RUN pnpm install --prod

WORKDIR /app/apps/api

EXPOSE 4000

CMD ["node", "dist/index.js"]
39 changes: 39 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "api",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx src/index.ts",
"build": "prisma generate && tsc",
"postinstall": "[ -f prisma/schema.prisma ] && prisma generate || true"
},
"keywords": [],
"author": "Ajeet Pratpa Singh",
"license": "ISC",
"packageManager": "pnpm@10.11.0",
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^24.5.1",
"prisma": "^5.22.0",
"tsx": "^4.20.3",
"typescript": "^5.9.2"
},
"dependencies": {
"@octokit/graphql": "^9.0.1",
"@opensox/shared": "workspace:*",
"@prisma/client": "^5.22.0",
"@trpc/server": "^11.5.1",
"cors": "^2.8.5",
"dotenv": "^16.5.0",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"helmet": "^7.2.0",
"jsonwebtoken": "^9.0.2",
"zod": "^4.1.9"
}
}
59 changes: 59 additions & 0 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model QueryCount {
id Int @id @default(1)
total_queries BigInt
}

model User {
id String @id @default(cuid())

email String @unique

firstName String

authMethod String

createdAt DateTime @default(now())

lastLogin DateTime @updatedAt

accounts Account[]
}

model Account {
id String @id @default(cuid())

userId String // Foreign key to User

type String // "oauth", "email", etc.

provider String // "google", "github", etc.

providerAccountId String // ID from the provider

refresh_token String?

access_token String?

expires_at Int?

token_type String?

scope String?

id_token String?

session_state String?

user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@unique([provider, providerAccountId])
}
26 changes: 26 additions & 0 deletions apps/api/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
import prisma from "./prisma.js";
import type { User } from "@prisma/client";

export async function createContext({
req,
res,
}: CreateExpressContextOptions): Promise<{
req: CreateExpressContextOptions["req"];
res: CreateExpressContextOptions["res"];
db: typeof prisma;
ip?: string;
user?: User | null;
}> {
const ip = req.ip || req.socket.remoteAddress || "unknown";

return {
req,
res,
db: prisma,
ip,
user: null,
};
}

export type Context = Awaited<ReturnType<typeof createContext>>;
118 changes: 118 additions & 0 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import dotenv from "dotenv";
import express from "express";
import type { Request, Response } from "express";
import * as trpcExpress from "@trpc/server/adapters/express";
import { appRouter } from "./routers/_app.js";
import { createContext } from "./context.js";
import prismaModule from "./prisma.js";
import cors from "cors";
import type { CorsOptions as CorsOptionsType } from "cors";
import rateLimit from "express-rate-limit";
import helmet from "helmet";
import ipBlocker from "./middleware/ipBlock.js";

dotenv.config();

const app = express();
const PORT = process.env.PORT || 4000;
const CORS_ORIGINS = process.env.CORS_ORIGINS
? process.env.CORS_ORIGINS.split(",")
: ["http://localhost:3000", "http://localhost:5000"];

// Security headers
app.use(helmet());
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
})
);

// Apply IP blocking middleware first
app.use(ipBlocker.middleware);

// Different rate limits for different endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: "Too many login attempts, please try again later",
standardHeaders: true,
legacyHeaders: false,
});

const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 30,
message: "Too many requests from this IP",
standardHeaders: true,
legacyHeaders: false,
});

// Request size limits
app.use(express.json({ limit: "10kb" }));
app.use(express.urlencoded({ limit: "10kb", extended: true }));

// CORS configuration
const corsOptions: CorsOptionsType = {
origin: (origin, callback) => {
if (!origin || CORS_ORIGINS.includes(origin)) {
callback(null, origin);
} else {
callback(new Error("Not allowed by CORS"));
}
},
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 86400, // 24 hours
};

app.use(cors(corsOptions));

// Blocked IPs endpoint (admin endpoint)
app.get("/admin/blocked-ips", (req: Request, res: Response) => {
const blockedIPs = ipBlocker.getBlockedIPs();
res.json({
blockedIPs: blockedIPs.map((ip) => ({
...ip,
blockedUntil: new Date(ip.blockedUntil).toISOString(),
})),
});
});

// Test endpoint
app.get("/test", apiLimiter, (req: Request, res: Response) => {
res.status(200).json({ status: "ok", message: "Test endpoint is working" });
});

// Connect to database
prismaModule.connectDB();

// Apply rate limiting to tRPC endpoints
app.use("/trpc", apiLimiter);

// tRPC middleware
app.use(
"/trpc",
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
})
);

// Global error handling
app.use((err: Error, req: Request, res: Response, next: Function) => {
console.error(err.stack);
res.status(500).json({
error: "Internal Server Error",
message: process.env.NODE_ENV === "development" ? err.message : undefined,
});
});

app.listen(PORT, () => {
console.log(`tRPC server running on http://localhost:${PORT}`);
});
Loading