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
46 changes: 46 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
FROM node:18-alpine AS base
WORKDIR /app
ENV NPM_CONFIG_AUDIT=false

# Install OS deps for prisma / openssl
RUN apk add --no-cache openssl

# Copy manifests and sources
COPY package*.json nx.json tsconfig.json tsconfig.base.json eslint.config.mjs ./
COPY apps ./apps
COPY packages ./packages
COPY prisma ./prisma

# Copy environment file if it exists
COPY .env* ./

# Install deps
RUN npm ci --legacy-peer-deps --no-audit

# Generate prisma client
RUN npx prisma generate

# Build services (all required apps)
RUN npx nx run-many --target=build --projects=api-gateway,auth-service,product-service,order-service,chat-service --configuration=production

# Runtime
FROM node:18-alpine AS runtime
WORKDIR /app
RUN apk add --no-cache openssl

# Copy built output and minimal files
COPY --from=base /app/apps /app/apps
COPY --from=base /app/node_modules /app/node_modules
COPY --from=base /app/prisma /app/prisma
COPY --from=base /app/.env* ./
COPY ecosystem.config.js ./ecosystem.config.js

ENV NODE_ENV=production
ENV PORT=8080
EXPOSE 8080

RUN npm i -g pm2

CMD pm2-runtime ecosystem.config.js


38 changes: 34 additions & 4 deletions apps/api-gateway/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import express from 'express';
import express, { Request } from 'express';
import * as path from 'path';
import cors from 'cors';
import proxy from 'express-http-proxy';
import httpProxy from 'http-proxy';
import { IncomingMessage } from 'http';
import { Socket } from 'net';
import morgan from 'morgan';
import rateLimit from 'express-rate-limit';
import cookieParser from 'cookie-parser';
Expand All @@ -11,7 +14,18 @@ const app = express();

app.use(
cors({
origin: ['http://localhost:3000', 'http://localhost:3001'],
origin: (origin, callback) => {
const allowedOriginsEnv = process.env.CORS_ORIGINS || '';
const allowedOrigins = allowedOriginsEnv
? allowedOriginsEnv.split(',').map((o) => o.trim())
: ['http://localhost:3000', 'http://localhost:3001'];

if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
}),
Expand All @@ -25,11 +39,11 @@ app.set('trust proxy', 1);
//Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: (req: any) => (req.user ? 1000 : 1000), // Limit each IP to
max: () => 1000, // Limit each IP to
message: { error: 'Too many requests. Please try again later' },
standardHeaders: true,
legacyHeaders: true,
keyGenerator: (req: any) => req.ip,
keyGenerator: (req: Request) => req.ip || 'unknown',
});

app.use(limiter);
Expand All @@ -44,6 +58,8 @@ app.get('/gateway-health', (req, res) => {
app.use('/product', proxy('http://localhost:6002'));
//app.use('/seller', proxy('http://localhost:6003'));
app.use('/order', proxy('http://localhost:6004'));

// Chat HTTP under /chat -> forwards to chat-service /api
app.use('/chat', proxy('http://localhost:6005'));

app.use('/', proxy('http://localhost:6001'));
Expand All @@ -59,4 +75,18 @@ const server = app.listen(port, () => {
console.error('Error initializing site config:', error);
}
});

// WS endpoint at /chat-ws -> forwards to chat-service WS root
const wsProxy = httpProxy.createProxyServer({
target: 'ws://localhost:6005',
changeOrigin: true,
ws: true,
});

server.on('upgrade', (req: IncomingMessage, socket: Socket, head: Buffer) => {
if (req.url && req.url.startsWith('/chat-ws')) {
req.url = req.url.replace(/^\/chat-ws/, '');
wsProxy.ws(req, socket, head);
}
});
server.on('error', console.error);
13 changes: 12 additions & 1 deletion apps/auth-service/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,18 @@ const app = express();

app.use(
cors({
origin: ['http://localhost:3000'],
origin: (origin, callback) => {
const allowedOriginsEnv = process.env.CORS_ORIGINS || '';
const allowedOrigins = allowedOriginsEnv
? allowedOriginsEnv.split(',').map((o) => o.trim())
: ['http://localhost:3000'];

if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
}),
Expand Down
19 changes: 19 additions & 0 deletions apps/chat-service/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import { startConsumer } from './chat-message.consumer';
import { createWebSocketServer } from './websocket';
import chatRoutes from './routes/chat.routes';

const app = express();
app.use(
cors({
origin: (origin, callback) => {
const allowedOriginsEnv = process.env.CORS_ORIGINS || '';
const allowedOrigins = allowedOriginsEnv
? allowedOriginsEnv.split(',').map((o) => o.trim())
: ['http://localhost:3000'];

if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
}),
);
app.use(express.json());
app.use(cookieParser());

Expand Down
2 changes: 1 addition & 1 deletion apps/order-service/src/controller/order.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface VerifySessionRequest extends Request {
};
}

interface WebhookRequest extends Request {
export interface WebhookRequest extends Request {
headers: {
'stripe-signature': string;
};
Expand Down
19 changes: 16 additions & 3 deletions apps/order-service/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import bodyParser from 'body-parser';
import { Request, Response, NextFunction } from 'express';
import { errorMiddleware } from '@packages/error-handler/error-middleware';
import orderRouter from './routes/order.route';
import { createOrder } from './controller/order.controller';
Expand All @@ -10,7 +11,18 @@ const app = express();

app.use(
cors({
origin: ['http://localhost:3000'],
origin: (origin, callback) => {
const allowedOriginsEnv = process.env.CORS_ORIGINS || '';
const allowedOrigins = allowedOriginsEnv
? allowedOriginsEnv.split(',').map((o) => o.trim())
: ['http://localhost:3000'];

if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
}),
Expand All @@ -20,11 +32,12 @@ app.use(
app.post(
'/api/create-order',
bodyParser.raw({ type: 'application/json' }),
(req, res, next) => {
(req: Request, res: Response, next: NextFunction) => {
(req as any).rawBody = req.body;
next();
},
createOrder,

(req: Request, res: Response, next: NextFunction) => createOrder(req as any, res, next),
);

app.use(express.json({ limit: '200mb' }));
Expand Down
13 changes: 12 additions & 1 deletion apps/product-service/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,18 @@ const app = express();

app.use(
cors({
origin: ['http://localhost:3000'],
origin: (origin, callback) => {
const allowedOriginsEnv = process.env.CORS_ORIGINS || '';
const allowedOrigins = allowedOriginsEnv
? allowedOriginsEnv.split(',').map((o) => o.trim())
: ['http://localhost:3000'];

if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
}),
Expand Down
2 changes: 1 addition & 1 deletion apps/seller-ui/src/context/websocket-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ seller, ch
useEffect(() => {
if (!seller?.id) return;

const wsUri = process.env.NEXT_PUBLIC_CHAT_WEBSOCKET_URI || 'ws://localhost:6005';
const wsUri = process.env.NEXT_PUBLIC_CHAT_WEBSOCKET_URI || 'ws://localhost:8080/chat';

setIsConnecting(true);
setError(null);
Expand Down
2 changes: 1 addition & 1 deletion apps/user-ui/CHAT_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Create a `.env.local` file in the `apps/user-ui` directory with the following:

```bash
# Chat Service Configuration
NEXT_PUBLIC_CHAT_WEBSOCKET_URI=ws://localhost:6005
NEXT_PUBLIC_CHAT_WEBSOCKET_URI=ws://localhost:8080/chat
```

## Features Implemented
Expand Down
2 changes: 1 addition & 1 deletion apps/user-ui/src/context/websocket-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ user, chil
useEffect(() => {
if (!user?.id) return;

const wsUri = process.env.NEXT_PUBLIC_CHAT_WEBSOCKET_URI || 'ws://localhost:6005';
const wsUri = process.env.NEXT_PUBLIC_CHAT_WEBSOCKET_URI || 'ws://localhost:8080/chat';

setIsConnecting(true);
setError(null);
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services:
eshop-app:
build: .
ports:
- '8080:8080'
env_file:
- .env
environment:
- NODE_ENV=production
29 changes: 29 additions & 0 deletions ecosystem.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module.exports = {
apps: [
{
name: 'auth-service',
script: 'apps/auth-service/dist/main.js',
env: { PORT: 6001 },
},
{
name: 'product-service',
script: 'apps/product-service/dist/main.js',
env: { PORT: 6002 },
},
{
name: 'order-service',
script: 'apps/order-service/dist/main.js',
env: { PORT: 6004 },
},
{
name: 'chat-service',
script: 'apps/chat-service/dist/main.js',
env: { PORT: 6005 },
},
{
name: 'api-gateway',
script: 'apps/api-gateway/dist/main.js',
env: { PORT: 8080 },
},
],
};
27 changes: 27 additions & 0 deletions env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Database Configuration
DATABASE_URL="mongodb://your-mongodb-host:27017/eshop"
MONGODB_URI="mongodb://your-mongodb-host:27017/eshop"

# JWT Configuration
JWT_SECRET="your-super-secret-jwt-key-change-this-in-production"
JWT_EXPIRES_IN="7d"

# Redis Configuration
REDIS_URL="redis://your-redis-host:6379"

# Kafka Configuration
KAFKA_BROKERS="your-kafka-host:9092"
KAFKA_CLIENT_ID="eshop-app"
KAFKA_GROUP_ID="eshop-group"

# API Gateway Configuration
API_GATEWAY_PORT=8080

# Service Ports
AUTH_SERVICE_PORT=6001
PRODUCT_SERVICE_PORT=6002
ORDER_SERVICE_PORT=6004
CHAT_SERVICE_PORT=6005

# Other Configuration
NODE_ENV=production
17 changes: 17 additions & 0 deletions render.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
services:
- type: web
name: eshop-monolith
env: docker
autoDeploy: true
plan: free
envVars:
- fromGroup: eshop-shared-vars
- key: NODE_ENV
value: production
buildFilter:
paths:
- apps/**
- packages/**
- prisma/**
- Dockerfile
- ecosystem.config.js
Loading