UNPKG

@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

2,108 lines (1,791 loc) 55.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.eggjsTemplate = void 0; exports.eggjsTemplate = { id: 'eggjs', name: 'eggjs', displayName: 'Egg.js', description: 'Enterprise-grade Node.js framework built on Koa with convention-over-configuration and multi-process model', language: 'typescript', framework: 'eggjs', version: '3.2.0', tags: ['nodejs', 'eggjs', 'koa', 'api', 'rest', 'enterprise', 'microservices', 'typescript'], port: 7001, dependencies: {}, features: ['plugin-system', 'multi-process', 'security', 'orm', 'scheduler', 'i18n', 'websocket', 'testing'], files: { // Package configuration 'package.json': `{ "name": "{{projectName}}", "version": "1.0.0", "description": "Enterprise-grade Egg.js application with TypeScript", "private": true, "egg": { "typescript": true, "declarations": true }, "scripts": { "start": "egg-scripts start --daemon --title=egg-server-{{projectName}}", "stop": "egg-scripts stop --title=egg-server-{{projectName}}", "dev": "egg-bin dev", "debug": "egg-bin debug", "test": "npm run lint -- --fix && npm run test-local", "test-local": "egg-bin test", "cov": "egg-bin cov", "tsc": "ets && tsc -p tsconfig.json", "ci": "npm run lint && npm run cov && npm run tsc", "autod": "autod", "lint": "eslint . --ext .ts,.js", "clean": "ets clean", "build": "npm run tsc", "docker:build": "docker build -t {{projectName}} .", "docker:run": "docker run -p 7001:7001 {{projectName}}", "migrate:new": "npx sequelize migration:generate --name", "migrate:up": "npx sequelize db:migrate", "migrate:down": "npx sequelize db:migrate:undo", "seed:create": "npx sequelize seed:generate --name", "seed:run": "npx sequelize db:seed:all" }, "dependencies": { "egg": "^3.17.5", "egg-scripts": "^2.19.0", "egg-sequelize": "^6.0.0", "egg-redis": "^2.4.0", "egg-session-redis": "^2.1.0", "egg-validate": "^2.0.2", "egg-cors": "^2.2.3", "egg-jwt": "^3.1.7", "egg-bcrypt": "^1.1.0", "egg-socket.io": "^4.1.6", "egg-view-nunjucks": "^2.3.0", "egg-multipart": "^3.3.0", "egg-oss": "^3.2.0", "egg-static": "^2.3.1", "egg-logrotator": "^3.2.0", "egg-schedule": "^3.7.0", "egg-i18n": "^2.1.0", "egg-security": "^2.10.0", "egg-jsonp": "^2.0.0", "egg-swagger-doc": "^2.3.2", "egg-bull": "^1.3.0", "egg-grpc": "^1.0.6", "egg-kafka": "^2.0.3", "egg-amqp": "^0.2.0", "egg-mongoose": "^3.3.1", "egg-elasticsearch": "^1.0.0", "mysql2": "^3.9.7", "pg": "^8.11.5", "ioredis": "^5.3.2", "jsonwebtoken": "^9.0.2", "uuid": "^9.0.1", "dayjs": "^1.11.10", "lodash": "^4.17.21", "class-validator": "^0.14.1", "class-transformer": "^0.5.1", "nodemailer": "^6.9.13", "qrcode": "^1.5.3", "sharp": "^0.33.3", "xlsx": "^0.18.5", "csv-parser": "^3.0.0", "sanitize-html": "^2.13.0" }, "devDependencies": { "@types/mocha": "^10.0.6", "@types/node": "^20.12.7", "@types/supertest": "^6.0.2", "@types/lodash": "^4.17.0", "@types/jsonwebtoken": "^9.0.6", "@types/nodemailer": "^6.4.14", "@types/qrcode": "^1.5.5", "egg-bin": "^6.5.2", "typescript": "^5.4.5", "eslint": "^8.57.0", "eslint-config-egg": "^13.0.0", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", "autod": "^3.1.2", "autod-egg": "^1.1.0", "egg-mock": "^5.12.2", "factory-girl": "^5.0.4", "supertest": "^7.0.0" }, "engines": { "node": ">=16.0.0" }, "ci": { "version": "16, 18, 20" } }`, // TypeScript configuration 'tsconfig.json': `{ "compileOnSave": true, "compilerOptions": { "target": "ES2020", "module": "commonjs", "strict": true, "noImplicitAny": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "charset": "utf8", "allowJs": false, "pretty": true, "lib": ["ES2020"], "noEmitOnError": false, "noUnusedLocals": true, "noUnusedParameters": true, "allowUnreachableCode": false, "allowUnusedLabels": false, "skipLibCheck": true, "skipDefaultLibCheck": true, "esModuleInterop": true, "resolveJsonModule": true, "strictPropertyInitialization": false, "strictNullChecks": true, "forceConsistentCasingInFileNames": true, "baseUrl": ".", "paths": { "@/*": ["app/*"], "@config/*": ["config/*"], "@lib/*": ["lib/*"], "@test/*": ["test/*"] } }, "include": [ "app/**/*.ts", "config/**/*.ts", "lib/**/*.ts", "test/**/*.ts", "typings/**/*.ts", "**/*.d.ts" ], "exclude": [ "node_modules", "dist", "logs", "run", "coverage" ] }`, // Egg configuration 'config/config.default.ts': `import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg'; export default (appInfo: EggAppInfo) => { const config = {} as PowerPartial<EggAppConfig>; // override config from framework / plugin config.keys = appInfo.name + '_{{projectName}}_1234567890'; // add your egg config in here config.middleware = ['errorHandler', 'notfoundHandler', 'gzip', 'requestLog']; // cluster config.cluster = { listen: { port: 7001, hostname: '0.0.0.0', }, }; // security config.security = { csrf: { enable: false, // Disable for API, enable for web }, domainWhiteList: ['http://localhost:3000', 'http://localhost:5173'], }; // cors config.cors = { credentials: true, allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS', }; // view config.view = { defaultViewEngine: 'nunjucks', mapping: { '.nj': 'nunjucks', }, }; // static config.static = { prefix: '/public/', dir: appInfo.baseDir + '/app/public', dynamic: true, preload: false, buffer: true, maxFiles: 1000, }; // multipart config.multipart = { mode: 'file', fileSize: '50mb', whitelist: [ '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.mp4', '.webm', '.avi', '.mov', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.zip', '.rar', '.7z', '.tar', '.gz', ], tmpdir: appInfo.baseDir + '/app/public/temp', cleanSchedule: { cron: '0 30 4 * * *', // Clean temp files at 4:30 AM daily }, }; // i18n config.i18n = { defaultLocale: 'en-US', dirs: [appInfo.baseDir + '/config/locale'], }; // logger config.logger = { level: 'INFO', consoleLevel: 'INFO', dir: appInfo.baseDir + '/logs', encoding: 'utf8', appLogName: \`\${appInfo.name}-web.log\`, coreLogName: 'egg-web.log', agentLogName: 'egg-agent.log', errorLogName: 'common-error.log', }; // logrotator config.logrotator = { filesRotateByHour: [], hourDelimiter: '-', filesRotateBySize: [], maxFileSize: 50 * 1024 * 1024, maxFiles: 10, rotateDuration: 60000, maxDays: 31, }; // custom config config.api = { prefix: '/api/v1', pagination: { pageSize: 20, maxPageSize: 100, }, }; // jwt config.jwt = { secret: process.env.JWT_SECRET || 'your-secret-key', expiresIn: '1h', refreshSecret: process.env.JWT_REFRESH_SECRET || 'your-refresh-secret', refreshExpiresIn: '7d', }; // add your special config in here const bizConfig = { sourceUrl: \`https://github.com/eggjs/examples/tree/master/\${appInfo.name}\`, }; // the return config will combines to EggAppConfig return { ...config, ...bizConfig, }; };`, // Local development config 'config/config.local.ts': `import { EggAppConfig, PowerPartial } from 'egg'; export default () => { const config: PowerPartial<EggAppConfig> = {}; // Database config.sequelize = { dialect: 'postgres', host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '5432'), database: process.env.DB_NAME || '{{projectName}}_dev', username: process.env.DB_USER || 'postgres', password: process.env.DB_PASSWORD || 'postgres', define: { underscored: true, freezeTableName: true, timestamps: true, }, timezone: '+08:00', logging: console.log, }; // Redis config.redis = { client: { port: parseInt(process.env.REDIS_PORT || '6379'), host: process.env.REDIS_HOST || 'localhost', password: process.env.REDIS_PASSWORD || '', db: 0, }, }; // Session config.sessionRedis = { key: 'EGG_SESSION', maxAge: 24 * 3600 * 1000, // 1 day httpOnly: true, encrypt: true, }; return config; };`, // Production config 'config/config.prod.ts': `import { EggAppConfig, PowerPartial } from 'egg'; export default () => { const config: PowerPartial<EggAppConfig> = {}; // Database config.sequelize = { dialect: 'postgres', host: process.env.DB_HOST!, port: parseInt(process.env.DB_PORT!), database: process.env.DB_NAME!, username: process.env.DB_USER!, password: process.env.DB_PASSWORD!, pool: { max: 20, min: 5, idle: 10000, }, define: { underscored: true, freezeTableName: true, timestamps: true, }, timezone: '+08:00', logging: false, }; // Redis config.redis = { client: { port: parseInt(process.env.REDIS_PORT!), host: process.env.REDIS_HOST!, password: process.env.REDIS_PASSWORD!, db: 0, }, }; // Logger config.logger = { level: 'ERROR', consoleLevel: 'ERROR', }; // Static config.static = { maxAge: 31536000, buffer: true, gzip: true, }; return config; };`, // Plugin configuration 'config/plugin.ts': `import { EggPlugin } from 'egg'; const plugin: EggPlugin = { // built-in plugins static: true, jsonp: true, view: true, nunjucks: { enable: true, package: 'egg-view-nunjucks', }, // database sequelize: { enable: true, package: 'egg-sequelize', }, // redis redis: { enable: true, package: 'egg-redis', }, sessionRedis: { enable: true, package: 'egg-session-redis', }, // validation validate: { enable: true, package: 'egg-validate', }, // security cors: { enable: true, package: 'egg-cors', }, // jwt jwt: { enable: true, package: 'egg-jwt', }, // bcrypt bcrypt: { enable: true, package: 'egg-bcrypt', }, // socket.io io: { enable: true, package: 'egg-socket.io', }, // file upload multipart: { enable: true, package: 'egg-multipart', }, // object storage oss: { enable: false, package: 'egg-oss', }, // scheduler schedule: { enable: true, package: 'egg-schedule', }, // i18n i18n: { enable: true, package: 'egg-i18n', }, // swagger swaggerdoc: { enable: true, package: 'egg-swagger-doc', }, // queue bull: { enable: true, package: 'egg-bull', }, }; export default plugin;`, // Application entry 'app.ts': `import { Application, IBoot } from 'egg'; export default class AppBootHook implements IBoot { private readonly app: Application; constructor(app: Application) { this.app = app; } configWillLoad() { // Ready to call configDidLoad, // Config, plugin files are referred, // this is the last chance to modify the config. } configDidLoad() { // Config, plugin files have loaded. } async didLoad() { // All files have loaded, start plugin here. } async willReady() { // All plugins have started, can do some thing before app ready. // Initialize database await this.app.model.sync({ alter: false }); // Initialize scheduled tasks this.app.logger.info('Scheduled tasks initialized'); } async didReady() { // Worker is ready, can do some things // don't need to block the app boot. const ctx = await this.app.createAnonymousContext(); // Cache warming await ctx.service.cache.warmUp(); this.app.logger.info('Application is ready'); } async serverDidReady() { // Server is listening. this.app.logger.info(\`Server running at http://localhost:\${this.app.config.cluster.listen.port}\`); } async beforeClose() { // Do some thing before app close. this.app.logger.info('Application is closing'); } }`, // Router 'app/router.ts': `import { Application } from 'egg'; export default (app: Application) => { const { controller, router, middleware, io } = app; const { api } = app.config; // Middleware const auth = middleware.auth(); const admin = middleware.admin(); const validate = middleware.validate(); const rateLimit = middleware.rateLimit(); // Health check router.get('/health', controller.health.check); router.get('/health/readiness', controller.health.readiness); router.get('/health/liveness', controller.health.liveness); // API routes const apiRouter = router.namespace(api.prefix); // Public routes apiRouter.post('/auth/register', validate, controller.auth.register); apiRouter.post('/auth/login', validate, controller.auth.login); apiRouter.post('/auth/refresh', controller.auth.refresh); apiRouter.get('/auth/verify/:token', controller.auth.verify); apiRouter.post('/auth/forgot-password', validate, controller.auth.forgotPassword); apiRouter.post('/auth/reset-password', validate, controller.auth.resetPassword); // Protected routes apiRouter.use(auth); // User routes apiRouter.get('/users/me', controller.user.getCurrentUser); apiRouter.put('/users/me', validate, controller.user.updateProfile); apiRouter.post('/users/change-password', validate, controller.user.changePassword); apiRouter.post('/users/avatar', controller.user.uploadAvatar); // Admin routes apiRouter.get('/users', admin, controller.user.list); apiRouter.get('/users/:id', admin, controller.user.get); apiRouter.put('/users/:id', admin, validate, controller.user.update); apiRouter.delete('/users/:id', admin, controller.user.delete); // Todo routes apiRouter.get('/todos', controller.todo.list); apiRouter.get('/todos/:id', controller.todo.get); apiRouter.post('/todos', validate, controller.todo.create); apiRouter.put('/todos/:id', validate, controller.todo.update); apiRouter.delete('/todos/:id', controller.todo.delete); apiRouter.post('/todos/bulk', controller.todo.bulkOperation); // File routes apiRouter.post('/files/upload', controller.file.upload); apiRouter.get('/files/:id', controller.file.download); apiRouter.delete('/files/:id', controller.file.delete); // WebSocket routes io.of('/').route('join', io.controller.chat.join); io.of('/').route('leave', io.controller.chat.leave); io.of('/').route('message', io.controller.chat.message); io.of('/').route('typing', io.controller.chat.typing); // Admin namespace io.of('/admin').use(async (ctx, next) => { // Admin authentication for WebSocket const token = ctx.socket.handshake.auth.token; if (!token) { ctx.socket.disconnect(); return; } await next(); }); io.of('/admin').route('stats', io.controller.admin.stats); io.of('/admin').route('logs', io.controller.admin.logs); };`, // Auth Controller 'app/controller/auth.ts': `import { Controller } from 'egg'; export default class AuthController extends Controller { async register() { const { ctx, service } = this; const { email, password, name } = ctx.request.body; // Check if user exists const existingUser = await service.user.findByEmail(email); if (existingUser) { ctx.throw(409, 'User already exists'); } // Create user const user = await service.user.create({ email, password, name, }); // Send verification email await service.email.sendVerificationEmail(user); // Generate tokens const { accessToken, refreshToken } = service.auth.generateTokens(user); ctx.body = { success: true, message: ctx.__('auth.register.success'), data: { user: user.toJSON(), accessToken, refreshToken, }, }; } async login() { const { ctx, service } = this; const { email, password } = ctx.request.body; // Validate credentials const user = await service.auth.validateUser(email, password); if (!user) { ctx.throw(401, ctx.__('auth.login.invalid')); } // Check if user is active if (!user.isActive) { ctx.throw(403, ctx.__('auth.login.inactive')); } // Update last login await service.user.updateLastLogin(user.id); // Generate tokens const { accessToken, refreshToken } = service.auth.generateTokens(user); // Set refresh token cookie ctx.cookies.set('refreshToken', refreshToken, { httpOnly: true, secure: ctx.app.config.env === 'prod', sameSite: 'lax', maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days }); ctx.body = { success: true, message: ctx.__('auth.login.success'), data: { user: user.toJSON(), accessToken, }, }; } async refresh() { const { ctx, service } = this; const refreshToken = ctx.cookies.get('refreshToken') || ctx.request.body.refreshToken; if (!refreshToken) { ctx.throw(401, 'Refresh token not provided'); } const tokens = await service.auth.refreshTokens(refreshToken); ctx.body = { success: true, data: tokens, }; } async verify() { const { ctx, service } = this; const { token } = ctx.params; await service.auth.verifyEmail(token); ctx.body = { success: true, message: ctx.__('auth.verify.success'), }; } async forgotPassword() { const { ctx, service } = this; const { email } = ctx.request.body; const user = await service.user.findByEmail(email); if (!user) { // Don't reveal if user exists ctx.body = { success: true, message: ctx.__('auth.forgot.sent'), }; return; } const resetToken = await service.auth.generateResetToken(user); await service.email.sendPasswordResetEmail(user, resetToken); ctx.body = { success: true, message: ctx.__('auth.forgot.sent'), }; } async resetPassword() { const { ctx, service } = this; const { token, password } = ctx.request.body; await service.auth.resetPassword(token, password); ctx.body = { success: true, message: ctx.__('auth.reset.success'), }; } }`, // User Controller 'app/controller/user.ts': `import { Controller } from 'egg'; export default class UserController extends Controller { async list() { const { ctx, service } = this; const { page = 1, limit = 20, search, role, status } = ctx.query; const result = await service.user.paginate({ page: Number(page), limit: Number(limit), search, role, status, }); ctx.body = { success: true, data: result, }; } async get() { const { ctx, service } = this; const { id } = ctx.params; const user = await service.user.findById(id); if (!user) { ctx.throw(404, 'User not found'); } ctx.body = { success: true, data: user.toJSON(), }; } async getCurrentUser() { const { ctx } = this; ctx.body = { success: true, data: ctx.state.user.toJSON(), }; } async updateProfile() { const { ctx, service } = this; const updates = ctx.request.body; // Remove protected fields delete updates.email; delete updates.password; delete updates.role; delete updates.isActive; const user = await service.user.update(ctx.state.user.id, updates); ctx.body = { success: true, message: ctx.__('user.update.success'), data: user.toJSON(), }; } async update() { const { ctx, service } = this; const { id } = ctx.params; const updates = ctx.request.body; const user = await service.user.update(id, updates); ctx.body = { success: true, message: ctx.__('user.update.success'), data: user.toJSON(), }; } async delete() { const { ctx, service } = this; const { id } = ctx.params; await service.user.delete(id); ctx.body = { success: true, message: ctx.__('user.delete.success'), }; } async changePassword() { const { ctx, service } = this; const { currentPassword, newPassword } = ctx.request.body; await service.user.changePassword( ctx.state.user.id, currentPassword, newPassword ); ctx.body = { success: true, message: ctx.__('user.password.changed'), }; } async uploadAvatar() { const { ctx, service } = this; const stream = await ctx.getFileStream(); const avatarUrl = await service.file.uploadImage(stream, { folder: 'avatars', resize: { width: 200, height: 200 }, }); await service.user.update(ctx.state.user.id, { avatarUrl }); ctx.body = { success: true, message: ctx.__('user.avatar.uploaded'), data: { avatarUrl }, }; } }`, // Todo Controller 'app/controller/todo.ts': `import { Controller } from 'egg'; export default class TodoController extends Controller { async list() { const { ctx, service } = this; const { page = 1, limit = 20, status, priority, sortBy = 'createdAt', order = 'DESC' } = ctx.query; const result = await service.todo.paginate({ userId: ctx.state.user.id, page: Number(page), limit: Number(limit), status, priority, sortBy, order: order.toUpperCase() as 'ASC' | 'DESC', }); ctx.body = { success: true, data: result, }; } async get() { const { ctx, service } = this; const { id } = ctx.params; const todo = await service.todo.findById(id, ctx.state.user.id); if (!todo) { ctx.throw(404, 'Todo not found'); } ctx.body = { success: true, data: todo.toJSON(), }; } async create() { const { ctx, service } = this; const data = { ...ctx.request.body, userId: ctx.state.user.id, }; const todo = await service.todo.create(data); ctx.body = { success: true, message: ctx.__('todo.create.success'), data: todo.toJSON(), }; } async update() { const { ctx, service } = this; const { id } = ctx.params; const updates = ctx.request.body; const todo = await service.todo.update(id, ctx.state.user.id, updates); ctx.body = { success: true, message: ctx.__('todo.update.success'), data: todo.toJSON(), }; } async delete() { const { ctx, service } = this; const { id } = ctx.params; await service.todo.delete(id, ctx.state.user.id); ctx.body = { success: true, message: ctx.__('todo.delete.success'), }; } async bulkOperation() { const { ctx, service } = this; const { operation, ids, updates } = ctx.request.body; let result; switch (operation) { case 'delete': result = await service.todo.bulkDelete(ids, ctx.state.user.id); break; case 'update': result = await service.todo.bulkUpdate(ids, ctx.state.user.id, updates); break; case 'complete': result = await service.todo.bulkUpdate(ids, ctx.state.user.id, { status: 'completed' }); break; default: ctx.throw(400, 'Invalid operation'); } ctx.body = { success: true, message: ctx.__('todo.bulk.success', { count: result }), data: { affected: result }, }; } }`, // Health Controller 'app/controller/health.ts': `import { Controller } from 'egg'; export default class HealthController extends Controller { async check() { const { ctx, app } = this; ctx.body = { status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), environment: app.config.env, version: app.config.pkg.version, }; } async readiness() { const { ctx, service } = this; const checks = await service.health.checkReadiness(); if (!checks.ready) { ctx.status = 503; } ctx.body = checks; } async liveness() { const { ctx, service } = this; const checks = await service.health.checkLiveness(); if (!checks.alive) { ctx.status = 503; } ctx.body = checks; } }`, // Auth Service 'app/service/auth.ts': `import { Service } from 'egg'; import { v4 as uuidv4 } from 'uuid'; export default class AuthService extends Service { generateTokens(user: any) { const { jwt } = this.app.config; const payload = { id: user.id, email: user.email, role: user.role, }; const accessToken = this.app.jwt.sign(payload, jwt.secret, { expiresIn: jwt.expiresIn, }); const refreshToken = this.app.jwt.sign( { id: user.id }, jwt.refreshSecret, { expiresIn: jwt.refreshExpiresIn } ); return { accessToken, refreshToken }; } async validateUser(email: string, password: string) { const user = await this.service.user.findByEmail(email); if (!user) { return null; } const isValid = await this.ctx.compare(password, user.password); if (!isValid) { return null; } return user; } async refreshTokens(refreshToken: string) { try { const decoded = this.app.jwt.verify(refreshToken, this.app.config.jwt.refreshSecret) as any; const user = await this.service.user.findById(decoded.id); if (!user || !user.isActive) { this.ctx.throw(401, 'Invalid refresh token'); } return this.generateTokens(user); } catch (error) { this.ctx.throw(401, 'Invalid refresh token'); } } async verifyEmail(token: string) { const user = await this.ctx.model.User.findOne({ where: { verificationToken: token }, }); if (!user) { this.ctx.throw(400, 'Invalid verification token'); } await user.update({ isVerified: true, verificationToken: null, }); return user; } async generateResetToken(user: any) { const resetToken = uuidv4(); const resetTokenExpiry = new Date(); resetTokenExpiry.setHours(resetTokenExpiry.getHours() + 1); await user.update({ resetToken, resetTokenExpiry, }); return resetToken; } async resetPassword(token: string, password: string) { const user = await this.ctx.model.User.findOne({ where: { resetToken: token }, }); if (!user || !user.resetTokenExpiry || user.resetTokenExpiry < new Date()) { this.ctx.throw(400, 'Invalid or expired reset token'); } const hashedPassword = await this.ctx.genHash(password); await user.update({ password: hashedPassword, resetToken: null, resetTokenExpiry: null, }); return user; } }`, // User Service 'app/service/user.ts': `import { Service } from 'egg'; import { v4 as uuidv4 } from 'uuid'; export default class UserService extends Service { async findById(id: string) { return await this.ctx.model.User.findByPk(id, { attributes: { exclude: ['password'] }, }); } async findByEmail(email: string) { return await this.ctx.model.User.findOne({ where: { email: email.toLowerCase() }, }); } async create(data: any) { const hashedPassword = await this.ctx.genHash(data.password); const verificationToken = uuidv4(); const user = await this.ctx.model.User.create({ ...data, email: data.email.toLowerCase(), password: hashedPassword, verificationToken, }); return user; } async update(id: string, updates: any) { const user = await this.findById(id); if (!user) { this.ctx.throw(404, 'User not found'); } await user.update(updates); return user; } async delete(id: string) { const user = await this.findById(id); if (!user) { this.ctx.throw(404, 'User not found'); } // Soft delete await user.update({ isActive: false }); return true; } async paginate(options: any) { const { page = 1, limit = 20, search, role, status } = options; const offset = (page - 1) * limit; const where: any = {}; if (search) { where[this.app.Sequelize.Op.or] = [ { name: { [this.app.Sequelize.Op.iLike]: \`%\${search}%\` } }, { email: { [this.app.Sequelize.Op.iLike]: \`%\${search}%\` } }, ]; } if (role) { where.role = role; } if (status === 'active') { where.isActive = true; } else if (status === 'inactive') { where.isActive = false; } const { count, rows } = await this.ctx.model.User.findAndCountAll({ where, limit, offset, order: [['createdAt', 'DESC']], attributes: { exclude: ['password'] }, }); return { data: rows, total: count, page, limit, totalPages: Math.ceil(count / limit), }; } async updateLastLogin(id: string) { await this.ctx.model.User.update( { lastLogin: new Date() }, { where: { id } } ); } async changePassword(id: string, currentPassword: string, newPassword: string) { const user = await this.ctx.model.User.findByPk(id); if (!user) { this.ctx.throw(404, 'User not found'); } const isValid = await this.ctx.compare(currentPassword, user.password); if (!isValid) { this.ctx.throw(400, 'Current password is incorrect'); } const hashedPassword = await this.ctx.genHash(newPassword); await user.update({ password: hashedPassword }); return true; } }`, // Todo Service 'app/service/todo.ts': `import { Service } from 'egg'; export default class TodoService extends Service { async findById(id: string, userId: string) { return await this.ctx.model.Todo.findOne({ where: { id, userId }, include: [{ model: this.ctx.model.User, as: 'user' }], }); } async create(data: any) { const todo = await this.ctx.model.Todo.create(data); return todo; } async update(id: string, userId: string, updates: any) { const todo = await this.findById(id, userId); if (!todo) { this.ctx.throw(404, 'Todo not found'); } await todo.update(updates); return todo; } async delete(id: string, userId: string) { const todo = await this.findById(id, userId); if (!todo) { this.ctx.throw(404, 'Todo not found'); } await todo.destroy(); return true; } async paginate(options: any) { const { userId, page = 1, limit = 20, status, priority, sortBy = 'createdAt', order = 'DESC' } = options; const offset = (page - 1) * limit; const where: any = { userId }; if (status) { where.status = status; } if (priority) { where.priority = priority; } const { count, rows } = await this.ctx.model.Todo.findAndCountAll({ where, limit, offset, order: [[sortBy, order]], }); return { data: rows, total: count, page, limit, totalPages: Math.ceil(count / limit), }; } async bulkDelete(ids: string[], userId: string) { const result = await this.ctx.model.Todo.destroy({ where: { id: { [this.app.Sequelize.Op.in]: ids }, userId, }, }); return result; } async bulkUpdate(ids: string[], userId: string, updates: any) { const [affected] = await this.ctx.model.Todo.update(updates, { where: { id: { [this.app.Sequelize.Op.in]: ids }, userId, }, }); return affected; } }`, // Cache Service 'app/service/cache.ts': `import { Service } from 'egg'; export default class CacheService extends Service { async get(key: string) { const value = await this.app.redis.get(key); if (value) { try { return JSON.parse(value); } catch { return value; } } return null; } async set(key: string, value: any, ttl = 3600) { const stringValue = typeof value === 'string' ? value : JSON.stringify(value); await this.app.redis.setex(key, ttl, stringValue); } async del(key: string | string[]) { if (Array.isArray(key)) { await this.app.redis.del(...key); } else { await this.app.redis.del(key); } } async exists(key: string) { const result = await this.app.redis.exists(key); return result === 1; } async incr(key: string) { return await this.app.redis.incr(key); } async expire(key: string, ttl: number) { await this.app.redis.expire(key, ttl); } async ttl(key: string) { return await this.app.redis.ttl(key); } async warmUp() { // Preload frequently accessed data this.logger.info('Cache warming up...'); // Example: Load configuration const config = await this.ctx.model.Config.findAll(); for (const item of config) { await this.set(\`config:\${item.key}\`, item.value); } this.logger.info('Cache warmed up successfully'); } }`, // Email Service 'app/service/email.ts': `import { Service } from 'egg'; import * as nodemailer from 'nodemailer'; export default class EmailService extends Service { private transporter: nodemailer.Transporter; constructor(ctx) { super(ctx); this.transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT || '587'), secure: process.env.SMTP_SECURE === 'true', auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, }); } async sendMail(options: nodemailer.SendMailOptions) { const mailOptions = { from: process.env.EMAIL_FROM || 'noreply@example.com', ...options, }; try { const info = await this.transporter.sendMail(mailOptions); this.logger.info('Email sent:', info.messageId); return info; } catch (error) { this.logger.error('Email send error:', error); throw error; } } async sendVerificationEmail(user: any) { const verificationUrl = \`\${process.env.APP_URL}/api/v1/auth/verify/\${user.verificationToken}\`; const html = await this.ctx.renderView('email/verification.nj', { user, verificationUrl, }); await this.sendMail({ to: user.email, subject: this.ctx.__('email.verification.subject'), html, }); } async sendPasswordResetEmail(user: any, resetToken: string) { const resetUrl = \`\${process.env.FRONTEND_URL}/reset-password?token=\${resetToken}\`; const html = await this.ctx.renderView('email/password-reset.nj', { user, resetUrl, }); await this.sendMail({ to: user.email, subject: this.ctx.__('email.passwordReset.subject'), html, }); } async sendWelcomeEmail(user: any) { const html = await this.ctx.renderView('email/welcome.nj', { user, }); await this.sendMail({ to: user.email, subject: this.ctx.__('email.welcome.subject'), html, }); } }`, // User Model 'app/model/user.ts': `import { Application } from 'egg'; export default (app: Application) => { const { STRING, INTEGER, DATE, BOOLEAN, ENUM, UUID, UUIDV4, JSON } = app.Sequelize; const User = app.model.define('user', { id: { type: UUID, defaultValue: UUIDV4, primaryKey: true, }, email: { type: STRING(255), unique: true, allowNull: false, validate: { isEmail: true, }, }, password: { type: STRING(255), allowNull: false, }, name: { type: STRING(100), allowNull: false, }, role: { type: ENUM('user', 'admin'), defaultValue: 'user', }, isActive: { type: BOOLEAN, defaultValue: true, }, isVerified: { type: BOOLEAN, defaultValue: false, }, verificationToken: { type: STRING(255), allowNull: true, }, resetToken: { type: STRING(255), allowNull: true, }, resetTokenExpiry: { type: DATE, allowNull: true, }, avatarUrl: { type: STRING(500), allowNull: true, }, phone: { type: STRING(20), allowNull: true, }, lastLogin: { type: DATE, allowNull: true, }, metadata: { type: JSON, allowNull: true, }, }, { timestamps: true, underscored: true, tableName: 'users', }); User.associate = () => { app.model.User.hasMany(app.model.Todo, { as: 'todos', foreignKey: 'userId' }); }; // Instance methods User.prototype.toJSON = function() { const values = this.get() as any; delete values.password; delete values.verificationToken; delete values.resetToken; delete values.resetTokenExpiry; return values; }; return User; };`, // Todo Model 'app/model/todo.ts': `import { Application } from 'egg'; export default (app: Application) => { const { STRING, INTEGER, DATE, BOOLEAN, ENUM, UUID, UUIDV4, TEXT, ARRAY } = app.Sequelize; const Todo = app.model.define('todo', { id: { type: UUID, defaultValue: UUIDV4, primaryKey: true, }, title: { type: STRING(200), allowNull: false, }, description: { type: TEXT, allowNull: true, }, status: { type: ENUM('pending', 'in_progress', 'completed', 'archived'), defaultValue: 'pending', }, priority: { type: ENUM('low', 'medium', 'high'), defaultValue: 'medium', }, dueDate: { type: DATE, allowNull: true, }, tags: { type: ARRAY(STRING), defaultValue: [], }, userId: { type: UUID, allowNull: false, }, }, { timestamps: true, underscored: true, tableName: 'todos', indexes: [ { fields: ['user_id'] }, { fields: ['status'] }, { fields: ['priority'] }, { fields: ['due_date'] }, ], }); Todo.associate = () => { app.model.Todo.belongsTo(app.model.User, { as: 'user', foreignKey: 'userId' }); }; return Todo; };`, // Auth Middleware 'app/middleware/auth.ts': `import { Context, Application } from 'egg'; export default (options?: any, app?: Application) => { return async (ctx: Context, next: () => Promise<any>) => { const token = ctx.headers.authorization?.replace('Bearer ', '') || ctx.query.token; if (!token) { ctx.throw(401, 'No token provided'); } try { const decoded = ctx.app.jwt.verify(token, ctx.app.config.jwt.secret) as any; const user = await ctx.service.user.findById(decoded.id); if (!user || !user.isActive) { ctx.throw(401, 'Invalid token'); } ctx.state.user = user; await next(); } catch (error) { ctx.throw(401, 'Invalid token'); } }; };`, // Rate Limit Middleware 'app/middleware/rateLimit.ts': `import { Context } from 'egg'; export default () => { return async (ctx: Context, next: () => Promise<any>) => { const { redis } = ctx.app; const key = \`rate_limit:\${ctx.ip}:\${ctx.path}\`; const limit = 100; // requests per minute const ttl = 60; // seconds const current = await redis.incr(key); if (current === 1) { await redis.expire(key, ttl); } if (current > limit) { ctx.status = 429; ctx.body = { success: false, message: 'Too many requests', }; return; } ctx.set('X-RateLimit-Limit', String(limit)); ctx.set('X-RateLimit-Remaining', String(limit - current)); ctx.set('X-RateLimit-Reset', String(Date.now() + ttl * 1000)); await next(); }; };`, // Error Handler Middleware 'app/middleware/errorHandler.ts': `import { Context } from 'egg'; export default () => { return async (ctx: Context, next: () => Promise<any>) => { try { await next(); } catch (err: any) { // Emit error event ctx.app.emit('error', err, ctx); // Normalize error const status = err.status || 500; const message = err.message || 'Internal Server Error'; // Error response ctx.status = status; ctx.body = { success: false, message, code: err.code || 'INTERNAL_ERROR', ...(ctx.app.config.env === 'local' && { stack: err.stack, details: err, }), }; // Log error if (status >= 500) { ctx.logger.error({ message: err.message, stack: err.stack, url: ctx.url, method: ctx.method, ip: ctx.ip, status, }); } } }; };`, // Schedule Tasks 'app/schedule/cleanup.ts': `import { Subscription } from 'egg'; export default class CleanupTask extends Subscription { static get schedule() { return { cron: '0 0 3 * * *', // Run at 3 AM daily type: 'worker', // Run on one worker only immediate: false, }; } async subscribe() { const { ctx } = this; ctx.logger.info('Starting cleanup task...'); try { // Clean expired sessions await this.cleanExpiredSessions(); // Clean old logs await this.cleanOldLogs(); // Clean temporary files await this.cleanTempFiles(); // Clean expired tokens await this.cleanExpiredTokens(); ctx.logger.info('Cleanup task completed successfully'); } catch (error) { ctx.logger.error('Cleanup task failed:', error); } } private async cleanExpiredSessions() { const { app } = this; const pattern = 'sess:*'; const keys = await app.redis.keys(pattern); let cleaned = 0; for (const key of keys) { const ttl = await app.redis.ttl(key); if (ttl === -1) { // No expiry set, check if expired const session = await app.redis.get(key); if (session) { try { const data = JSON.parse(session); if (data.expire && Date.now() > data.expire) { await app.redis.del(key); cleaned++; } } catch { // Invalid session data await app.redis.del(key); cleaned++; } } } } this.logger.info(\`Cleaned \${cleaned} expired sessions\`); } private async cleanOldLogs() { // Implementation depends on your logging setup this.logger.info('Cleaning old logs...'); } private async cleanTempFiles() { // Clean temporary upload files older than 24 hours const fs = require('fs').promises; const path = require('path'); const tempDir = path.join(this.app.baseDir, 'app/public/temp'); try { const files = await fs.readdir(tempDir); const now = Date.now(); const maxAge = 24 * 60 * 60 * 1000; // 24 hours for (const file of files) { const filePath = path.join(tempDir, file); const stats = await fs.stat(filePath); if (now - stats.mtimeMs > maxAge) { await fs.unlink(filePath); } } } catch (error) { this.logger.error('Error cleaning temp files:', error); } } private async cleanExpiredTokens() { const { ctx } = this; // Clean expired reset tokens await ctx.model.User.update( { resetToken: null, resetTokenExpiry: null, }, { where: { resetTokenExpiry: { [ctx.app.Sequelize.Op.lt]: new Date(), }, }, } ); } }`, // WebSocket Controller 'app/io/controller/chat.ts': `import { Controller } from 'egg'; export default class ChatController extends Controller { async join() { const { ctx, app } = this; const { room } = ctx.args[0]; const { socket } = ctx; socket.join(room); // Notify others in room socket.to(room).emit('user_joined', { userId: ctx.state.user.id, username: ctx.state.user.name, timestamp: new Date(), }); await ctx.socket.emit('joined', { room, message: 'Successfully joined room', }); } async leave() { const { ctx } = this; const { room } = ctx.args[0]; const { socket } = ctx; socket.leave(room); // Notify others in room socket.to(room).emit('user_left', { userId: ctx.state.user.id, username: ctx.state.user.name, timestamp: new Date(), }); await ctx.socket.emit('left', { room, message: 'Successfully left room', }); } async message() { const { ctx } = this; const { room, message } = ctx.args[0]; const { socket } = ctx; // Save message to database const savedMessage = await ctx.service.chat.saveMessage({ userId: ctx.state.user.id, room, message, }); // Broadcast to room socket.to(room).emit('message', { id: savedMessage.id, userId: ctx.state.user.id, username: ctx.state.user.name, message, timestamp: savedMessage.createdAt, }); await ctx.socket.emit('message_sent', { id: savedMessage.id, timestamp: savedMessage.createdAt, }); } async typing() { const { ctx } = this; const { room, isTyping } = ctx.args[0]; const { socket } = ctx; socket.to(room).emit('typing', { userId: ctx.state.user.id, username: ctx.state.user.name, isTyping, }); } }`, // Environment variables '.env.example': `# Application NODE_ENV=development EGG_SERVER_ENV=local # Server PORT=7001 WORKERS=1 # Database DB_HOST=localhost DB_PORT=5432 DB_NAME={{projectName}}_dev DB_USER=postgres DB_PASSWORD=postgres # Redis REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= # JWT JWT_SECRET=your-jwt-secret-key JWT_REFRESH_SECRET=your-refresh-secret-key # Email SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_SECURE=false SMTP_USER=your-email@gmail.com SMTP_PASS=your-app-password EMAIL_FROM=noreply@example.com # URLs APP_URL=http://localhost:7001 FRONTEND_URL=http://localhost:3000 # File Upload MAX_FILE_SIZE=52428800 UPLOAD_DIR=app/public/uploads # Object Storage (Optional) OSS_REGION= OSS_ACCESS_KEY_ID= OSS_ACCESS_KEY_SECRET= OSS_BUCKET= # Third Party APIs WECHAT_APP_ID= WECHAT_APP_SECRET= ALIPAY_APP_ID= ALIPAY_PRIVATE_KEY= ALIPAY_PUBLIC_KEY=`, // Docker configuration 'Dockerfile': `# Build stage FROM node:18-alpine AS builder WORKDIR /app # Copy package files COPY package*.json ./ # Install dependencies RUN npm ci # Copy source code COPY . . # Build TypeScript RUN npm run tsc # Production stage FROM node:18-alpine # Install dumb-init RUN apk add --no-cache dumb-init # Create app directory WORKDIR /app # Copy package files COPY package*.json ./ # Install production dependencies RUN npm ci --only=production && npm cache clean --force # Copy built application COPY --from=builder /app/app ./app COPY --from=builder /app/config ./config COPY --from=builder /app/typings ./typings # Copy other necessary files COPY app.ts ./ COPY .env.example ./.env # Create necessary directories RUN mkdir -p logs run app/public/uploads app/public/temp # Expose port EXPOSE 7001 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD node -e "require('http').get('http://localhost:7001/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); })" # Start application ENTRYPOINT ["dumb-init", "--"] CMD ["npm", "start"]`, 'docker-compose.yml': `version: '3.8' services: app: build: . container_name: {{projectName}}-api ports: - "\${PORT:-7001}:7001" environment: - NODE_ENV=production - EGG_SERVER_ENV=prod - DB_HOST=postgres - DB_PORT=5432 - DB_NAME=\${DB_NAME:-{{projectName}}} - DB_USER=\${DB_USER:-postgres} - DB_PASSWORD=\${DB_PASSWORD:-postgres} - REDIS_HOST=redis - REDIS_PORT=6379 env_file: - .env depends_on: postgres: condition: service_healthy redis: condition: service_healthy volumes: - ./logs:/app/logs - ./run:/app/run - ./app/public/uploads:/app/app/public/uploads restart: unless-stopped networks: - app-network postgres: image: postgres:16-alpine container_name: {{projectName}}-db environment: - POSTGRES_USER=\${DB_USER:-postgres} - POSTGRES_PASSWORD=\${DB_PASSWORD:-postgres} - POSTGRES_DB=\${DB_NAME:-{{projectName}}} ports: - "\${DB_PORT:-5432}:5432" volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U \${DB_USER:-postgres}"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped networks: - app-network redis: image: redis:7-alpine container_name: {{projectName}}-redis command: redis-server --appendonly yes --requirepass \${REDIS_PASSWORD:-} ports: - "\${REDIS_PORT:-6379}:6379" volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped networks: - app-network nginx: image: nginx:alpine container_name: {{projectName}}-nginx ports: - "80:80" - "443:443" volumes: