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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# PostgreSQL Database Connection URL
DATABASE_URL=postgresql://user:password@ep-sample-12345.eu-central-1.aws.neon.tech/neondb?sslmode=require

# Managed Better Auth (Neon Auth)
NEON_AUTH_BASE_URL=https://ep-sample-12345.neonauth.eu-central-1.aws.neon.tech/neondb/auth
NEON_AUTH_COOKIE_SECRET=your-secure-cookie-secret-at-least-32-characters-long

# Core API Service Configuration
PORT=5000
NODE_ENV=development
Expand Down
31 changes: 30 additions & 1 deletion api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-rate-limit": "^8.6.2",
"pg": "^8.23.0",
"reflect-metadata": "^0.2.2",
"typeorm": "^1.1.0"
Expand Down
31 changes: 31 additions & 0 deletions api/src/controllers/categories.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Request, Response, NextFunction } from 'express';
import { categoriesService } from '../services/categories.service';

export class CategoriesController {
async getAllCategories(req: Request, res: Response, next: NextFunction) {
try {
const categories = await categoriesService.getAllCategories();
res.json({
success: true,
data: categories,
});
} catch (error) {
next(error);
}
}

async getCategoryBySlug(req: Request, res: Response, next: NextFunction) {
try {
const { slug } = req.params;
const category = await categoriesService.getCategoryBySlug(slug as string);
res.json({
success: true,
data: category,
});
} catch (error) {
next(error);
}
}
}

export const categoriesController = new CategoriesController();
55 changes: 55 additions & 0 deletions api/src/controllers/products.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Request, Response, NextFunction } from 'express';
import { productsService, ProductQueryParams } from '../services/products.service';

export class ProductsController {
async getProducts(req: Request, res: Response, next: NextFunction) {
try {
const {
page,
limit,
search,
category,
manufacturer,
minPrice,
maxPrice,
sortBy,
} = req.query;

const queryParams: ProductQueryParams = {
page: page ? Number(page) : undefined,
limit: limit ? Number(limit) : undefined,
search: typeof search === 'string' ? search : undefined,
category: typeof category === 'string' ? category : undefined,
manufacturer: typeof manufacturer === 'string' ? manufacturer : undefined,
minPrice: minPrice ? Number(minPrice) : undefined,
maxPrice: maxPrice ? Number(maxPrice) : undefined,
sortBy: sortBy as ProductQueryParams['sortBy'],
};
Comment on lines +18 to +27

const result = await productsService.getProducts(queryParams);

res.json({
success: true,
...result,
});
} catch (error) {
next(error);
}
}

async getProductByIdentifier(req: Request, res: Response, next: NextFunction) {
try {
const { identifier } = req.params;
const product = await productsService.getProductByIdentifier(identifier as string);

res.json({
success: true,
data: product,
});
} catch (error) {
next(error);
}
}
}

export const productsController = new ProductsController();
74 changes: 74 additions & 0 deletions api/src/controllers/projects.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Request, Response, NextFunction } from 'express';
import { projectsService } from '../services/projects.service';

export class ProjectsController {
async getProjects(req: Request, res: Response, next: NextFunction) {
try {
const userId = typeof req.query.userId === 'string' ? req.query.userId : undefined;
const projects = await projectsService.getProjects(userId);

Comment on lines +5 to +9
res.json({
success: true,
data: projects,
});
} catch (error) {
next(error);
}
}

async getProjectById(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const project = await projectsService.getProjectById(id as string);

res.json({
success: true,
data: project,
});
} catch (error) {
next(error);
}
}

async createProject(req: Request, res: Response, next: NextFunction) {
try {
const { title, userId, data } = req.body;
const project = await projectsService.createProject({ title, userId, data });

res.status(201).json({
success: true,
data: project,
});
} catch (error) {
next(error);
}
}

async updateProject(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const { title, data } = req.body;
const project = await projectsService.updateProject(id as string, { title, data });

res.json({
success: true,
data: project,
});
} catch (error) {
next(error);
}
}

async deleteProject(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const result = await projectsService.deleteProject(id as string);

res.json(result);
} catch (error) {
next(error);
}
}
}

export const projectsController = new ProjectsController();
38 changes: 26 additions & 12 deletions api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import 'reflect-metadata';
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import { AppDataSource } from './data-source';
import "reflect-metadata";
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import { AppDataSource } from "./data-source";
import routes from "./routes";
import { apiRateLimiter } from "./middlewares/rateLimiter";
import { errorHandler } from "./middlewares/errorHandler";

dotenv.config();

Expand All @@ -12,31 +15,42 @@ const port = process.env.PORT || 5000;
app.use(cors());
app.use(express.json());

app.get('/health', (req, res) => {
app.use(apiRateLimiter);

app.get("/health", (req, res) => {
res.json({
status: 'ok',
service: 'core-api',
status: "ok",
service: "core-api",
db_connected: AppDataSource.isInitialized,
timestamp: new Date().toISOString(),
});
});

app.use("/api/v1/core", routes);
app.use("/", routes);

app.use(errorHandler);
Comment on lines +29 to +32

async function startServer() {
try {
if (process.env.DATABASE_URL) {
await AppDataSource.initialize();
console.log('[Database] Connected to PostgreSQL via TypeORM');
console.log("[Database] Connected to PostgreSQL via TypeORM");
} else {
console.warn('[Database] DATABASE_URL not set. Running without database connection.');
console.warn(
"[Database] DATABASE_URL not set. Running without database connection.",
);
}

app.listen(port, () => {
console.log(`[Server] Core API Service listening on port ${port}`);
});
} catch (error) {
console.error('[Database] Connection failed:', error);
console.error("[Database] Connection failed:", error);
app.listen(port, () => {
console.log(`[Server] Core API Service listening on port ${port} (database offline)`);
console.log(
`[Server] Core API Service listening on port ${port} (database offline)`,
);
});
}
}
Expand Down
52 changes: 52 additions & 0 deletions api/src/middlewares/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Request, Response, NextFunction } from 'express';

export interface AuthenticatedRequest extends Request {
user?: {
id: string;
email?: string;
name?: string;
};
}

export function optionalAuth(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) {
const userId = req.headers['x-user-id'] as string;
const userEmail = req.headers['x-user-email'] as string;
const userName = req.headers['x-user-name'] as string;

if (userId) {
req.user = {
id: userId,
email: userEmail,
name: userName,
};
}
Comment on lines +16 to +26

next();
}

export function requireAuth(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) {
const userId = req.headers['x-user-id'] as string;

if (!userId) {
return res.status(401).json({
error: 'unauthorized',
message: 'Authentication required. Please sign in to perform this action.',
});
}

req.user = {
id: userId,
email: req.headers['x-user-email'] as string,
name: req.headers['x-user-name'] as string,
};

next();
}
19 changes: 19 additions & 0 deletions api/src/middlewares/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Request, Response, NextFunction } from 'express';

export function errorHandler(
err: any,
req: Request,
res: Response,
next: NextFunction
) {
console.error('[Error]', err);

const statusCode = err.status || err.statusCode || 500;
const message = err.message || 'Internal Server Error';

res.status(statusCode).json({
error: err.name || 'APIError',
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
}
16 changes: 16 additions & 0 deletions api/src/middlewares/rateLimiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import rateLimit from 'express-rate-limit';
import { Request, Response } from 'express';

export const apiRateLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute window
max: 60, // Limit each IP to 60 requests per window
standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
legacyHeaders: false, // Disable `X-RateLimit-*` headers
handler: (req: Request, res: Response) => {
res.status(429).json({
error: 'rate_limit_exceeded',
message: 'Too many requests. Please wait a moment before trying again.',
retry_after_seconds: 60,
});
},
});
Loading