@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
1,807 lines (1,635 loc) • 84.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.hapiTypeScriptTemplate = void 0;
exports.hapiTypeScriptTemplate = {
id: 'hapi-ts',
name: 'Hapi.js + TypeScript',
displayName: 'Hapi.js + TypeScript',
description: 'Enterprise-grade Hapi.js API server with TypeScript, built-in validation, caching, security, and plugin architecture',
framework: 'hapi',
language: 'typescript',
version: '21.3.9',
tags: ['typescript', 'hapi', 'api', 'rest', 'validation', 'security', 'caching'],
port: 3000,
features: ['authentication', 'database', 'validation', 'logging', 'documentation', 'testing', 'caching', 'security'],
dependencies: {
'@hapi/hapi': '^21.3.9',
'@hapi/joi': '^17.1.1',
'@hapi/boom': '^10.0.1',
'@hapi/inert': '^7.1.0',
'@hapi/vision': '^7.0.3',
'@hapi/good': '^9.0.1',
'@hapi/catbox-redis': '^7.0.2',
'@hapi/basic': '^7.0.2',
'@hapi/jwt': '^3.2.0',
'@hapi/bell': '^13.0.1',
'hapi-swagger': '^17.2.1',
'hapi-rate-limit': '^6.1.0',
'bcrypt': '^5.1.1',
'jsonwebtoken': '^9.0.2',
'redis': '^4.6.13',
'winston': '^3.11.0',
'dotenv': '^16.4.5',
'helmet': '^7.1.0',
'cors': '^2.8.5',
'uuid': '^9.0.1',
'@prisma/client': '^5.8.1'
},
devDependencies: {
'@types/hapi__hapi': '^20.0.13',
'@types/hapi__joi': '^17.1.14',
'@types/hapi__boom': '^9.0.4',
'@types/hapi__inert': '^5.2.10',
'@types/hapi__vision': '^5.5.7',
'@types/hapi__good': '^8.1.5',
'@types/hapi__basic': '^5.0.4',
'@types/hapi__jwt': '^2.0.4',
'@types/hapi__bell': '^10.0.6',
'@types/bcrypt': '^5.0.2',
'@types/jsonwebtoken': '^9.0.6',
'@types/node': '^20.17.0',
'@types/redis': '^4.0.11',
'typescript': '^5.3.3',
'ts-node': '^10.9.2',
'tsx': '^4.7.0',
'nodemon': '^3.0.2',
'@types/lab': '^18.1.4',
'@hapi/lab': '^25.2.0',
'@hapi/code': '^9.0.3',
'rimraf': '^5.0.5',
'prisma': '^5.8.1',
'@types/uuid': '^9.0.7'
},
files: {
'package.json': `{
"name": "{{serviceName}}",
"version": "1.0.0",
"description": "Hapi.js TypeScript API server with built-in validation, caching, and security",
"main": "dist/server.js",
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "nodemon --exec ts-node src/server.ts",
"test": "lab -v --reporter console --output stdout --coverage --threshold 80",
"test:watch": "lab -v --reporter console --output stdout --watch",
"lint": "tsc --noEmit",
"clean": "rimraf dist",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:migrate": "prisma migrate dev",
"db:migrate:deploy": "prisma migrate deploy",
"db:migrate:reset": "prisma migrate reset",
"db:studio": "prisma studio",
"db:seed": "tsx prisma/seed.ts"
},
"keywords": ["hapi", "typescript", "api", "validation", "caching", "security"],
"author": "{{author}}",
"license": "MIT"
}`,
'tsconfig.json': `{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"]
}`,
'src/server.ts': `import Hapi from '@hapi/hapi';
import { configureServer } from './config/server';
import { logger } from './utils/logger';
import { gracefulShutdown } from './utils/gracefulShutdown';
const init = async (): Promise<Hapi.Server> => {
const server = await configureServer();
await server.start();
logger.info(\`Server running on \${server.info.uri}\`);
return server;
};
process.on('unhandledRejection', (err) => {
logger.error('Unhandled rejection:', err);
process.exit(1);
});
process.on('SIGTERM', () => gracefulShutdown());
process.on('SIGINT', () => gracefulShutdown());
init().catch((error) => {
logger.error('Failed to start server:', error);
process.exit(1);
});`,
'src/config/server.ts': `import Hapi from '@hapi/hapi';
import { loadEnvironment } from './environment';
import { registerPlugins } from './plugins';
import { setupRoutes } from './routes';
import { setupCache } from './cache';
export const configureServer = async (): Promise<Hapi.Server> => {
const env = loadEnvironment();
const server = Hapi.server({
port: env.PORT,
host: env.HOST,
routes: {
cors: {
origin: ['*'],
headers: ['Accept', 'Authorization', 'Content-Type', 'If-None-Match'],
exposedHeaders: ['WWW-Authenticate', 'Server-Authorization'],
additionalExposedHeaders: ['Cache-Control'],
maxAge: 60,
credentials: true
},
validate: {
failAction: async (request, h, err) => {
if (process.env.NODE_ENV === 'production') {
throw err;
}
console.error(err);
throw err;
}
}
}
});
// Setup cache
await setupCache(server);
// Register plugins
await registerPlugins(server);
// Setup routes
setupRoutes(server);
return server;
};`,
'src/config/environment.ts': `import dotenv from 'dotenv';
import Joi from '@hapi/joi';
dotenv.config();
interface Environment {
NODE_ENV: string;
PORT: number;
HOST: string;
JWT_SECRET: string;
JWT_EXPIRATION: string;
REDIS_URL: string;
CACHE_TTL: number;
API_RATE_LIMIT: number;
LOG_LEVEL: string;
}
const envSchema = Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
PORT: Joi.number().default(3000),
HOST: Joi.string().default('localhost'),
JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION: Joi.string().default('24h'),
REDIS_URL: Joi.string().default('redis://localhost:6379'),
CACHE_TTL: Joi.number().default(300000), // 5 minutes
API_RATE_LIMIT: Joi.number().default(100),
LOG_LEVEL: Joi.string().valid('error', 'warn', 'info', 'debug').default('info')
}).unknown();
export const loadEnvironment = (): Environment => {
const { error, value } = envSchema.validate(process.env);
if (error) {
throw new Error(\`Environment validation error: \${error.message}\`);
}
return value;
};`,
'src/config/plugins.ts': `import Hapi from '@hapi/hapi';
import Inert from '@hapi/inert';
import Vision from '@hapi/vision';
import Good from '@hapi/good';
import Basic from '@hapi/basic';
import Jwt from '@hapi/jwt';
import HapiSwagger from 'hapi-swagger';
import RateLimit from 'hapi-rate-limit';
import { loadEnvironment } from './environment';
import { validateUser } from '../auth/strategies';
import { logger } from '../utils/logger';
export const registerPlugins = async (server: Hapi.Server): Promise<void> => {
const env = loadEnvironment();
// Static files and templating
await server.register([Inert, Vision]);
// Logging
await server.register({
plugin: Good,
options: {
ops: {
interval: 1000
},
reporters: {
console: [{
module: '@hapi/good-squeeze',
name: 'Squeeze',
args: [{ log: '*', response: '*' }]
}, {
module: '@hapi/good-console'
}, 'stdout']
}
}
});
// Rate limiting
await server.register({
plugin: RateLimit,
options: {
userLimit: env.API_RATE_LIMIT,
userCache: {
expiresIn: 60000 // 1 minute
},
addressOnly: true,
pathLimit: false,
userAttribute: 'id',
userWhitelist: ['admin'],
addressWhitelist: ['127.0.0.1', '::1'],
trustProxy: true,
ipWhitelist: []
}
});
// Authentication
await server.register([Basic, Jwt]);
// JWT Strategy
server.auth.strategy('jwt', 'jwt', {
keys: env.JWT_SECRET,
verify: {
aud: false,
iss: false,
sub: false,
nbf: true,
exp: true,
maxAgeSec: 86400, // 24 hours
timeSkewSec: 15
},
validate: validateUser
});
// Basic Auth Strategy
server.auth.strategy('basic', 'basic', {
validate: async (request, username, password) => {
// Implement basic auth validation
return { isValid: false, credentials: {} };
}
});
server.auth.default('jwt');
// Swagger documentation
const swaggerOptions: HapiSwagger.RegisterOptions = {
info: {
title: 'Hapi.js TypeScript API',
version: '1.0.0',
description: 'API documentation for Hapi.js TypeScript server'
},
schemes: env.NODE_ENV === 'production' ? ['https'] : ['http'],
host: env.NODE_ENV === 'production' ? 'api.example.com' : \`\${env.HOST}:\${env.PORT}\`,
documentationPath: '/docs',
grouping: 'tags',
tags: [
{ name: 'auth', description: 'Authentication endpoints' },
{ name: 'users', description: 'User management' },
{ name: 'health', description: 'Health check endpoints' }
]
};
await server.register({
plugin: HapiSwagger,
options: swaggerOptions
});
logger.info('All plugins registered successfully');
};`,
'src/config/routes.ts': `import Hapi from '@hapi/hapi';
import { authRoutes } from '../routes/auth';
import { userRoutes } from '../routes/users';
import { postRoutes } from '../routes/posts';
import { healthRoutes } from '../routes/health';
export const setupRoutes = (server: Hapi.Server): void => {
// Health check routes (no auth required)
server.route(healthRoutes);
// Authentication routes
server.route(authRoutes);
// User routes (requires auth)
server.route(userRoutes);
// Post routes (requires auth)
server.route(postRoutes);
};`,
'src/config/cache.ts': `import Hapi from '@hapi/hapi';
import { loadEnvironment } from './environment';
export const setupCache = async (server: Hapi.Server): Promise<void> => {
const env = loadEnvironment();
// Register Redis cache
await server.register({
plugin: require('@hapi/catbox-redis'),
options: {
uri: env.REDIS_URL,
partition: 'cache'
}
});
// Define cache policies
const cache = server.cache({
segment: 'sessions',
expiresIn: env.CACHE_TTL
});
const userCache = server.cache({
segment: 'users',
expiresIn: 600000 // 10 minutes
});
// Make caches available to routes
server.app.cache = cache;
server.app.userCache = userCache;
};`,
'src/auth/strategies.ts': `import Hapi from '@hapi/hapi';
import Boom from '@hapi/boom';
import { Role } from '@prisma/client';
import { UserService } from '../services/userService';
export interface JWTPayload {
id: string;
email: string;
role: Role;
iat: number;
exp: number;
}
export const validateUser = async (
decoded: JWTPayload,
request: Hapi.Request,
h: Hapi.ResponseToolkit
) => {
try {
const userService = new UserService();
const user = await userService.findById(decoded.id);
if (!user) {
return { isValid: false };
}
return {
isValid: true,
credentials: {
id: user.id,
email: user.email,
role: user.role,
scope: [user.role.toLowerCase()] // For role-based access control
}
};
} catch (error) {
return { isValid: false };
}
};
export const requireRole = (role: Role | string) => {
return (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
const { credentials } = request.auth;
const requiredRole = typeof role === 'string' ? role.toLowerCase() : role.toLowerCase();
if (!credentials?.scope?.includes(requiredRole)) {
throw Boom.forbidden('Insufficient permissions');
}
return h.continue;
};
};`,
'src/routes/auth.ts': `import Hapi from '@hapi/hapi';
import Joi from '@hapi/joi';
import { AuthController } from '../controllers/authController';
const authController = new AuthController();
export const authRoutes: Hapi.ServerRoute[] = [
{
method: 'POST',
path: '/auth/login',
options: {
auth: false,
description: 'User login',
notes: 'Authenticate user and return JWT token',
tags: ['api', 'auth'],
validate: {
payload: Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(6).required()
})
},
response: {
schema: Joi.object({
token: Joi.string().required(),
user: Joi.object({
id: Joi.string().required(),
email: Joi.string().email().required(),
role: Joi.string().required()
}).required()
})
}
},
handler: authController.login
},
{
method: 'POST',
path: '/auth/register',
options: {
auth: false,
description: 'User registration',
notes: 'Register a new user account',
tags: ['api', 'auth'],
validate: {
payload: Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
name: Joi.string().required()
})
},
response: {
schema: Joi.object({
message: Joi.string().required(),
user: Joi.object({
id: Joi.string().required(),
email: Joi.string().email().required(),
name: Joi.string().required()
}).required()
})
}
},
handler: authController.register
},
{
method: 'POST',
path: '/auth/refresh',
options: {
description: 'Refresh JWT token',
notes: 'Get a new JWT token using the current valid token',
tags: ['api', 'auth'],
response: {
schema: Joi.object({
token: Joi.string().required()
})
}
},
handler: authController.refresh
},
{
method: 'POST',
path: '/auth/logout',
options: {
description: 'User logout',
notes: 'Invalidate the current JWT token',
tags: ['api', 'auth'],
response: {
schema: Joi.object({
message: Joi.string().required()
})
}
},
handler: authController.logout
}
];`,
'src/routes/users.ts': `import Hapi from '@hapi/hapi';
import Joi from '@hapi/joi';
import { UserController } from '../controllers/userController';
import { requireRole } from '../auth/strategies';
const userController = new UserController();
export const userRoutes: Hapi.ServerRoute[] = [
{
method: 'GET',
path: '/users',
options: {
description: 'Get all users',
notes: 'Retrieve a list of all users (admin only)',
tags: ['api', 'users'],
pre: [{ method: requireRole('admin') }],
validate: {
query: Joi.object({
page: Joi.number().min(1).default(1),
limit: Joi.number().min(1).max(100).default(10),
search: Joi.string().optional()
})
},
response: {
schema: Joi.object({
users: Joi.array().items(
Joi.object({
id: Joi.string().required(),
email: Joi.string().email().required(),
name: Joi.string().required(),
role: Joi.string().required(),
createdAt: Joi.date().required()
})
).required(),
pagination: Joi.object({
page: Joi.number().required(),
limit: Joi.number().required(),
total: Joi.number().required(),
pages: Joi.number().required()
}).required()
})
}
},
handler: userController.getUsers
},
{
method: 'GET',
path: '/users/me',
options: {
description: 'Get current user profile',
notes: 'Retrieve the profile of the currently authenticated user',
tags: ['api', 'users'],
response: {
schema: Joi.object({
id: Joi.string().required(),
email: Joi.string().email().required(),
name: Joi.string().required(),
role: Joi.string().required(),
createdAt: Joi.date().required(),
updatedAt: Joi.date().required()
})
}
},
handler: userController.getCurrentUser
},
{
method: 'GET',
path: '/users/{id}',
options: {
description: 'Get user by ID',
notes: 'Retrieve a specific user by their ID',
tags: ['api', 'users'],
validate: {
params: Joi.object({
id: Joi.string().uuid().required()
})
},
response: {
schema: Joi.object({
id: Joi.string().required(),
email: Joi.string().email().required(),
name: Joi.string().required(),
role: Joi.string().required(),
createdAt: Joi.date().required(),
updatedAt: Joi.date().required()
})
}
},
handler: userController.getUserById
},
{
method: 'PUT',
path: '/users/me',
options: {
description: 'Update current user profile',
notes: 'Update the profile of the currently authenticated user',
tags: ['api', 'users'],
validate: {
payload: Joi.object({
name: Joi.string().optional(),
email: Joi.string().email().optional()
})
},
response: {
schema: Joi.object({
id: Joi.string().required(),
email: Joi.string().email().required(),
name: Joi.string().required(),
role: Joi.string().required(),
updatedAt: Joi.date().required()
})
}
},
handler: userController.updateCurrentUser
},
{
method: 'DELETE',
path: '/users/{id}',
options: {
description: 'Delete user',
notes: 'Delete a user account (admin only)',
tags: ['api', 'users'],
pre: [{ method: requireRole('admin') }],
validate: {
params: Joi.object({
id: Joi.string().uuid().required()
})
},
response: {
schema: Joi.object({
message: Joi.string().required()
})
}
},
handler: userController.deleteUser
}
];`,
'src/routes/posts.ts': `import Hapi from '@hapi/hapi';
import Joi from '@hapi/joi';
import { PostController } from '../controllers/postController';
import { requireRole } from '../auth/strategies';
const postController = new PostController();
export const postRoutes: Hapi.ServerRoute[] = [
{
method: 'GET',
path: '/posts',
options: {
auth: false,
description: 'Get published posts',
notes: 'Retrieve a list of published posts with pagination',
tags: ['api', 'posts'],
validate: {
query: Joi.object({
page: Joi.number().min(1).default(1),
limit: Joi.number().min(1).max(50).default(10),
search: Joi.string().optional(),
author: Joi.string().optional()
})
},
response: {
schema: Joi.object({
posts: Joi.array().items(
Joi.object({
id: Joi.string().required(),
title: Joi.string().required(),
excerpt: Joi.string().allow(null),
slug: Joi.string().required(),
status: Joi.string().required(),
publishedAt: Joi.date().allow(null),
createdAt: Joi.date().required(),
author: Joi.object({
id: Joi.string().required(),
name: Joi.string().required(),
email: Joi.string().email().required()
}).required()
})
).required(),
pagination: Joi.object({
page: Joi.number().required(),
limit: Joi.number().required(),
total: Joi.number().required(),
pages: Joi.number().required()
}).required()
})
}
},
handler: postController.getPosts
},
{
method: 'GET',
path: '/posts/{slug}',
options: {
auth: false,
description: 'Get post by slug',
notes: 'Retrieve a specific published post by its slug',
tags: ['api', 'posts'],
validate: {
params: Joi.object({
slug: Joi.string().required()
})
},
response: {
schema: Joi.object({
id: Joi.string().required(),
title: Joi.string().required(),
content: Joi.string().allow(null),
excerpt: Joi.string().allow(null),
slug: Joi.string().required(),
status: Joi.string().required(),
publishedAt: Joi.date().allow(null),
createdAt: Joi.date().required(),
updatedAt: Joi.date().required(),
author: Joi.object({
id: Joi.string().required(),
name: Joi.string().required(),
email: Joi.string().email().required()
}).required()
})
}
},
handler: postController.getPostBySlug
},
{
method: 'GET',
path: '/posts/my',
options: {
description: 'Get current user posts',
notes: 'Retrieve posts created by the authenticated user',
tags: ['api', 'posts'],
validate: {
query: Joi.object({
page: Joi.number().min(1).default(1),
limit: Joi.number().min(1).max(50).default(10),
status: Joi.string().valid('DRAFT', 'PUBLISHED', 'ARCHIVED').optional()
})
}
},
handler: postController.getMyPosts
},
{
method: 'POST',
path: '/posts',
options: {
description: 'Create a new post',
notes: 'Create a new post (authenticated users only)',
tags: ['api', 'posts'],
validate: {
payload: Joi.object({
title: Joi.string().required(),
content: Joi.string().optional(),
excerpt: Joi.string().optional(),
status: Joi.string().valid('DRAFT', 'PUBLISHED').default('DRAFT')
})
},
response: {
schema: Joi.object({
id: Joi.string().required(),
title: Joi.string().required(),
content: Joi.string().allow(null),
excerpt: Joi.string().allow(null),
slug: Joi.string().required(),
status: Joi.string().required(),
publishedAt: Joi.date().allow(null),
createdAt: Joi.date().required(),
updatedAt: Joi.date().required()
})
}
},
handler: postController.createPost
},
{
method: 'PUT',
path: '/posts/{id}',
options: {
description: 'Update a post',
notes: 'Update a post (author or admin only)',
tags: ['api', 'posts'],
validate: {
params: Joi.object({
id: Joi.string().required()
}),
payload: Joi.object({
title: Joi.string().optional(),
content: Joi.string().optional(),
excerpt: Joi.string().optional(),
status: Joi.string().valid('DRAFT', 'PUBLISHED', 'ARCHIVED').optional()
})
}
},
handler: postController.updatePost
},
{
method: 'DELETE',
path: '/posts/{id}',
options: {
description: 'Delete a post',
notes: 'Delete a post (author or admin only)',
tags: ['api', 'posts'],
validate: {
params: Joi.object({
id: Joi.string().required()
})
},
response: {
schema: Joi.object({
message: Joi.string().required()
})
}
},
handler: postController.deletePost
},
{
method: 'GET',
path: '/admin/posts',
options: {
description: 'Get all posts (admin)',
notes: 'Retrieve all posts including drafts (admin only)',
tags: ['api', 'posts', 'admin'],
pre: [{ method: requireRole('admin') }],
validate: {
query: Joi.object({
page: Joi.number().min(1).default(1),
limit: Joi.number().min(1).max(100).default(20),
status: Joi.string().valid('DRAFT', 'PUBLISHED', 'ARCHIVED').optional(),
author: Joi.string().optional()
})
}
},
handler: postController.getAllPosts
}
];`,
'src/routes/health.ts': `import Hapi from '@hapi/hapi';
import Joi from '@hapi/joi';
import { HealthController } from '../controllers/healthController';
const healthController = new HealthController();
export const healthRoutes: Hapi.ServerRoute[] = [
{
method: 'GET',
path: '/health',
options: {
auth: false,
description: 'Basic health check',
notes: 'Returns basic server health status',
tags: ['api', 'health'],
response: {
schema: Joi.object({
status: Joi.string().required(),
timestamp: Joi.date().required()
})
}
},
handler: healthController.basic
},
{
method: 'GET',
path: '/health/detailed',
options: {
auth: false,
description: 'Detailed health check',
notes: 'Returns detailed server health information including dependencies',
tags: ['api', 'health'],
response: {
schema: Joi.object({
status: Joi.string().required(),
timestamp: Joi.date().required(),
uptime: Joi.number().required(),
memory: Joi.object().required(),
database: Joi.object().required(),
cache: Joi.object().required()
})
}
},
handler: healthController.detailed
},
{
method: 'GET',
path: '/health/ready',
options: {
auth: false,
description: 'Readiness probe',
notes: 'Kubernetes readiness probe endpoint',
tags: ['api', 'health']
},
handler: healthController.ready
},
{
method: 'GET',
path: '/health/live',
options: {
auth: false,
description: 'Liveness probe',
notes: 'Kubernetes liveness probe endpoint',
tags: ['api', 'health']
},
handler: healthController.live
}
];`,
'src/controllers/authController.ts': `import Hapi from '@hapi/hapi';
import Boom from '@hapi/boom';
import { AuthService } from '../services/authService';
import { logger } from '../utils/logger';
export class AuthController {
private authService: AuthService;
constructor() {
this.authService = new AuthService();
}
login = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { email, password } = request.payload as { email: string; password: string };
const result = await this.authService.login(email, password);
if (!result) {
throw Boom.unauthorized('Invalid credentials');
}
logger.info(\`User logged in: \${email}\`);
return h.response(result).code(200);
} catch (error) {
logger.error('Login error:', error);
throw error;
}
};
register = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { email, password, name } = request.payload as {
email: string;
password: string;
name: string;
};
const result = await this.authService.register(email, password, name);
logger.info(\`User registered: \${email}\`);
return h.response(result).code(201);
} catch (error) {
logger.error('Registration error:', error);
if (error.message.includes('already exists')) {
throw Boom.conflict('User already exists');
}
throw Boom.badImplementation('Registration failed');
}
};
refresh = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { credentials } = request.auth;
const newToken = await this.authService.refreshToken(credentials.id);
return h.response({ token: newToken }).code(200);
} catch (error) {
logger.error('Token refresh error:', error);
throw Boom.unauthorized('Failed to refresh token');
}
};
logout = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { credentials } = request.auth;
await this.authService.logout(credentials.id);
logger.info(\`User logged out: \${credentials.email}\`);
return h.response({ message: 'Logged out successfully' }).code(200);
} catch (error) {
logger.error('Logout error:', error);
throw Boom.badImplementation('Logout failed');
}
};
}`,
'src/controllers/userController.ts': `import Hapi from '@hapi/hapi';
import Boom from '@hapi/boom';
import { UserService } from '../services/userService';
import { logger } from '../utils/logger';
export class UserController {
private userService: UserService;
constructor() {
this.userService = new UserService();
}
getUsers = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { page, limit, search } = request.query as {
page: number;
limit: number;
search?: string;
};
const result = await this.userService.getUsers(page, limit, search);
return h.response(result).code(200);
} catch (error) {
logger.error('Get users error:', error);
throw Boom.badImplementation('Failed to retrieve users');
}
};
getCurrentUser = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { credentials } = request.auth;
const user = await this.userService.findById(credentials.id);
if (!user) {
throw Boom.notFound('User not found');
}
return h.response(user).code(200);
} catch (error) {
logger.error('Get current user error:', error);
throw error;
}
};
getUserById = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { id } = request.params as { id: string };
const user = await this.userService.findById(id);
if (!user) {
throw Boom.notFound('User not found');
}
return h.response(user).code(200);
} catch (error) {
logger.error('Get user by ID error:', error);
throw error;
}
};
updateCurrentUser = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { credentials } = request.auth;
const updates = request.payload as { name?: string; email?: string };
const user = await this.userService.updateUser(credentials.id, updates);
logger.info(\`User updated: \${credentials.email}\`);
return h.response(user).code(200);
} catch (error) {
logger.error('Update user error:', error);
throw Boom.badImplementation('Failed to update user');
}
};
deleteUser = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { id } = request.params as { id: string };
await this.userService.deleteUser(id);
logger.info(\`User deleted: \${id}\`);
return h.response({ message: 'User deleted successfully' }).code(200);
} catch (error) {
logger.error('Delete user error:', error);
throw Boom.badImplementation('Failed to delete user');
}
};
}`,
'src/controllers/postController.ts': `import Hapi from '@hapi/hapi';
import Boom from '@hapi/boom';
import { PostService } from '../services/postService';
import { logger } from '../utils/logger';
export class PostController {
private postService: PostService;
constructor() {
this.postService = new PostService();
}
getPosts = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { page, limit, search, author } = request.query as {
page: number;
limit: number;
search?: string;
author?: string;
};
const result = await this.postService.getPublishedPosts(page, limit, search, author);
return h.response(result).code(200);
} catch (error) {
logger.error('Get posts error:', error);
throw Boom.badImplementation('Failed to retrieve posts');
}
};
getPostBySlug = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { slug } = request.params as { slug: string };
const post = await this.postService.getPostBySlug(slug);
if (!post) {
throw Boom.notFound('Post not found');
}
return h.response(post).code(200);
} catch (error) {
logger.error('Get post by slug error:', error);
throw error;
}
};
getMyPosts = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { credentials } = request.auth;
const { page, limit, status } = request.query as {
page: number;
limit: number;
status?: string;
};
const result = await this.postService.getUserPosts(credentials.id, page, limit, status);
return h.response(result).code(200);
} catch (error) {
logger.error('Get my posts error:', error);
throw Boom.badImplementation('Failed to retrieve your posts');
}
};
createPost = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { credentials } = request.auth;
const postData = request.payload as {
title: string;
content?: string;
excerpt?: string;
status?: string;
};
const post = await this.postService.createPost(credentials.id, postData);
logger.info(\`Post created: \${post.title} by \${credentials.email}\`);
return h.response(post).code(201);
} catch (error) {
logger.error('Create post error:', error);
throw Boom.badImplementation('Failed to create post');
}
};
updatePost = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { credentials } = request.auth;
const { id } = request.params as { id: string };
const updates = request.payload as {
title?: string;
content?: string;
excerpt?: string;
status?: string;
};
const post = await this.postService.updatePost(id, credentials.id, credentials.role, updates);
logger.info(\`Post updated: \${id} by \${credentials.email}\`);
return h.response(post).code(200);
} catch (error) {
logger.error('Update post error:', error);
if (error.message.includes('not found')) {
throw Boom.notFound('Post not found');
}
if (error.message.includes('permission')) {
throw Boom.forbidden('Insufficient permissions');
}
throw Boom.badImplementation('Failed to update post');
}
};
deletePost = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { credentials } = request.auth;
const { id } = request.params as { id: string };
await this.postService.deletePost(id, credentials.id, credentials.role);
logger.info(\`Post deleted: \${id} by \${credentials.email}\`);
return h.response({ message: 'Post deleted successfully' }).code(200);
} catch (error) {
logger.error('Delete post error:', error);
if (error.message.includes('not found')) {
throw Boom.notFound('Post not found');
}
if (error.message.includes('permission')) {
throw Boom.forbidden('Insufficient permissions');
}
throw Boom.badImplementation('Failed to delete post');
}
};
getAllPosts = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
try {
const { page, limit, status, author } = request.query as {
page: number;
limit: number;
status?: string;
author?: string;
};
const result = await this.postService.getAllPosts(page, limit, status, author);
return h.response(result).code(200);
} catch (error) {
logger.error('Get all posts error:', error);
throw Boom.badImplementation('Failed to retrieve all posts');
}
};
}`,
'src/controllers/healthController.ts': `import Hapi from '@hapi/hapi';
import { HealthService } from '../services/healthService';
export class HealthController {
private healthService: HealthService;
constructor() {
this.healthService = new HealthService();
}
basic = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
const health = await this.healthService.getBasicHealth();
return h.response(health).code(200);
};
detailed = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
const health = await this.healthService.getDetailedHealth();
return h.response(health).code(200);
};
ready = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
const isReady = await this.healthService.isReady();
return h.response(isReady ? 'Ready' : 'Not Ready').code(isReady ? 200 : 503);
};
live = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
const isLive = await this.healthService.isLive();
return h.response(isLive ? 'Live' : 'Not Live').code(isLive ? 200 : 503);
};
}`,
'src/services/authService.ts': `import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { Role } from '@prisma/client';
import { UserService } from './userService';
import { loadEnvironment } from '../config/environment';
export interface LoginResult {
token: string;
user: {
id: string;
email: string;
role: Role;
};
}
export interface RegisterResult {
message: string;
user: {
id: string;
email: string;
name: string;
};
}
export class AuthService {
private userService: UserService;
private env = loadEnvironment();
constructor() {
this.userService = new UserService();
}
async login(email: string, password: string): Promise<LoginResult | null> {
const user = await this.userService.findByEmail(email);
if (!user || !await bcrypt.compare(password, user.password)) {
return null;
}
const token = this.generateToken(user.id, user.email, user.role);
return {
token,
user: {
id: user.id,
email: user.email,
role: user.role
}
};
}
async register(email: string, password: string, name: string): Promise<RegisterResult> {
const existingUser = await this.userService.findByEmail(email);
if (existingUser) {
throw new Error('User already exists');
}
const hashedPassword = await bcrypt.hash(password, 12);
const user = await this.userService.createUser({
email,
password: hashedPassword,
name,
role: Role.USER
});
return {
message: 'User registered successfully',
user: {
id: user.id,
email: user.email,
name: user.name
}
};
}
async refreshToken(userId: string): Promise<string> {
const user = await this.userService.findById(userId);
if (!user) {
throw new Error('User not found');
}
return this.generateToken(user.id, user.email, user.role);
}
async logout(userId: string): Promise<void> {
// Implement token blacklisting or session invalidation
// For now, we'll just log the action
console.log(\`User \${userId} logged out\`);
}
private generateToken(id: string, email: string, role: Role): string {
return jwt.sign(
{ id, email, role },
this.env.JWT_SECRET,
{ expiresIn: this.env.JWT_EXPIRATION }
);
}
}`,
'src/services/userService.ts': `import { User, Role, Prisma } from '@prisma/client';
import { prisma } from '../lib/prisma';
export interface CreateUserData {
email: string;
password: string;
name: string;
role: Role;
}
export interface UpdateUserData {
name?: string;
email?: string;
}
export interface PaginatedUsers {
users: Omit<User, 'password'>[];
pagination: {
page: number;
limit: number;
total: number;
pages: number;
};
}
export interface UserWithProfile extends Omit<User, 'password'> {
profile?: {
id: string;
bio: string | null;
avatar: string | null;
website: string | null;
location: string | null;
birthday: Date | null;
phone: string | null;
createdAt: Date;
updatedAt: Date;
} | null;
}
export class UserService {
async findById(id: string, includeProfile = false): Promise<UserWithProfile | null> {
try {
const user = await prisma.user.findUnique({
where: { id },
include: {
profile: includeProfile
}
});
if (!user) return null;
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
} catch (error) {
throw new Error(\`Failed to find user by ID: \${error.message}\`);
}
}
async findByEmail(email: string, includeProfile = false): Promise<User | null> {
try {
const user = await prisma.user.findUnique({
where: { email },
include: {
profile: includeProfile
}
});
return user;
} catch (error) {
throw new Error(\`Failed to find user by email: \${error.message}\`);
}
}
async createUser(userData: CreateUserData): Promise<Omit<User, 'password'>> {
try {
const user = await prisma.user.create({
data: userData
});
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
throw new Error('User with this email already exists');
}
}
throw new Error(\`Failed to create user: \${error.message}\`);
}
}
async updateUser(id: string, updates: UpdateUserData): Promise<Omit<User, 'password'>> {
try {
const user = await prisma.user.update({
where: { id },
data: updates
});
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2025') {
throw new Error('User not found');
}
if (error.code === 'P2002') {
throw new Error('Email already taken by another user');
}
}
throw new Error(\`Failed to update user: \${error.message}\`);
}
}
async deleteUser(id: string): Promise<void> {
try {
await prisma.user.delete({
where: { id }
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2025') {
throw new Error('User not found');
}
}
throw new Error(\`Failed to delete user: \${error.message}\`);
}
}
async getUsers(page: number = 1, limit: number = 10, search?: string): Promise<PaginatedUsers> {
try {
const skip = (page - 1) * limit;
const where: Prisma.UserWhereInput = search
? {
OR: [
{ name: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } }
]
}
: {};
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
select: {
id: true,
email: true,
name: true,
role: true,
createdAt: true,
updatedAt: true
}
}),
prisma.user.count({ where })
]);
const pages = Math.ceil(total / limit);
return {
users,
pagination: {
page,
limit,
total,
pages
}
};
} catch (error) {
throw new Error(\`Failed to get users: \${error.message}\`);
}
}
async getUserStats(): Promise<{
total: number;
byRole: Record<Role, number>;
recentCount: number;
}> {
try {
const [total, usersByRole, recentCount] = await Promise.all([
prisma.user.count(),
prisma.user.groupBy({
by: ['role'],
_count: { role: true }
}),
prisma.user.count({
where: {
createdAt: {
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // Last 7 days
}
}
})
]);
const byRole = usersByRole.reduce((acc, item) => {
acc[item.role] = item._count.role;
return acc;
}, {} as Record<Role, number>);
// Ensure all roles are represented
Object.values(Role).forEach(role => {
if (!(role in byRole)) {
byRole[role] = 0;
}
});
return {
total,
byRole,
recentCount
};
} catch (error) {
throw new Error(\`Failed to get user stats: \${error.message}\`);
}
}
async createUserProfile(userId: string, profileData: {
bio?: string;
avatar?: string;
website?: string;
location?: string;
birthday?: Date;
phone?: string;
}): Promise<UserWithProfile> {
try {
const user = await prisma.user.update({
where: { id: userId },
data: {
profile: {
create: profileData
}
},
include: {
profile: true
}
});
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2025') {
throw new Error('User not found');
}
if (error.code === 'P2002') {
throw new Error('User already has a profile');
}
}
throw new Error(\`Failed to create user profile: \${error.message}\`);
}
}
async updateUserProfile(userId: string, profileData: {
bio?: string;
avatar?: string;
website?: string;
location?: string;
birthday?: Date;
phone?: string;
}): Promise<UserWithProfile> {
try {
const user = await prisma.user.update({
where: { id: userId },
data: {
profile: {
upsert: {
create: profileData,
update: profileData
}
}
},
include: {
profile: true
}
});
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2025') {
throw new Error('User not found');
}
}
throw new Error(\`Failed to update user profile: \${error.message}\`);
}
}
}`,
'src/services/postService.ts': `import { Post, PostStatus, Role, Prisma } from '@prisma/client';
import { prisma } from '../lib/prisma';
export interface CreatePostData {
title: string;
content?: string;
excerpt?: string;
status?: PostStatus;
}
export interface UpdatePostData {
title?: string;
content?: string;
excerpt?: string;
status?: PostStatus;
}
export interface PaginatedPosts {
posts: (Post & {
author: {
id: string;
name: string;
email: string;
};
})[];
pagination: {
page: number;
limit: number;
total: number;
pages: number;
};
}
export class PostService {
private generateSlug(title: string): string {
return title
.toLowerCase()
.replace(/[^a-z0-9 -]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
}
private async ensureUniqueSlug(baseSlug: string, excludeId?: string): Promise<string> {
let slug = baseSlug;
let counter = 1;
while (true) {
const existing = await prisma.post.findFirst({
where: {
slug,
id: excludeId ? { not: excludeId } : undefined
}
});
if (!existing) {
return slug;
}
slug = \`\${baseSlug}-\${counter}\`;
counter++;
}
}
async getPublishedPosts(
page: number = 1,
limit: number = 10,
search?: string,
authorId?: string
): Promise<PaginatedPosts> {
try {
const skip = (page - 1) * limit;
const where: Prisma.PostWhereInput = {
status: PostStatus.PUBLISHED,
...(search && {
OR: [
{ title: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } },
{ excerpt: { contains: search, mode: 'insensitive' } }
]
}),
...(authorId && { authorId })
};
const [posts, total] = await Promise.all([
prisma.post.findMany({
where,
skip,
take: limit,
orderBy: { publishedAt: 'desc' },
include: {
author: {
select: {
id: true,
name: true,
email: true
}
}
}
}),
prisma.post.count({ where })
]);
const pages = Math.ceil(total / limit);
return {
posts,
pagination: {
page,
limit,
total,
pages
}
};
} catch (error) {
throw new Error(\`Failed to get published posts: \${error.message}\`);
}
}
async getPostBySlug(slug: string): Promise<(Post & {
author: {
id: string;
name: string;
email: string;
};
}) | null> {
try {
const post = await prisma.post.findUnique({
where: {
slug,
status: PostStatus.PUBLISHED
},
include: {
author: {
select: {
id: true,
name: true,
email: true
}
}
}