first commit
This commit is contained in:
425
.cursor/rules/customrule.mdc
Normal file
425
.cursor/rules/customrule.mdc
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
---
|
||||||
|
# Rules and Memories - Strict Coding Practices
|
||||||
|
|
||||||
|
## 🧠 Memory Bank - Key Concepts to Remember
|
||||||
|
|
||||||
|
### Core Principles
|
||||||
|
- **Type Safety First**: Every variable, function, and object must have explicit types
|
||||||
|
- **Functional Programming**: Use map, filter, reduce instead of loops
|
||||||
|
- **Pure Functions**: Functions should not have side effects
|
||||||
|
- **Immutable Data**: Never mutate existing objects, create new ones
|
||||||
|
- **Single Responsibility**: Each function/class should do one thing well
|
||||||
|
- **Explicit Over Implicit**: Always be explicit about types, returns, and behaviors
|
||||||
|
|
||||||
|
### Critical Reminders
|
||||||
|
- **NO `any`** - If you need `any`, you need better types
|
||||||
|
- **NO `var`** - Always use `const` or `let`
|
||||||
|
- **NO loops** - Use functional methods instead
|
||||||
|
- **NO console.log** - Use proper logging service
|
||||||
|
- **NO magic numbers** - Use named constants
|
||||||
|
- **NO deep nesting** - Maximum 3 levels of indentation
|
||||||
|
- **NO long functions** - Maximum 50 lines
|
||||||
|
- **NO long files** - Maximum 300 lines
|
||||||
|
|
||||||
|
## 📏 Measurement Rules
|
||||||
|
|
||||||
|
### File Size Limits
|
||||||
|
- **Maximum file length**: 1000 lines
|
||||||
|
- **Maximum function length**: 50 lines
|
||||||
|
- **Maximum nesting depth**: 3 levels
|
||||||
|
- **Maximum parameters**: 5 per function
|
||||||
|
- **Maximum line length**: 100 characters
|
||||||
|
|
||||||
|
### Performance Thresholds
|
||||||
|
- **Function complexity**: Maximum 10 cyclomatic complexity
|
||||||
|
- **Test coverage**: Minimum 80%
|
||||||
|
- **Bundle size**: Monitor and keep reasonable
|
||||||
|
- **Response time**: API endpoints < 200ms
|
||||||
|
- **Database queries**: Maximum 3 per endpoint
|
||||||
|
|
||||||
|
## 🎯 Quality Gates - What Blocks Deployment
|
||||||
|
|
||||||
|
### Pre-commit Blockers
|
||||||
|
- [ ] ESLint errors (any severity)
|
||||||
|
- [ ] TypeScript compilation errors
|
||||||
|
- [ ] Failing tests (unit/integration)
|
||||||
|
- [ ] Prettier formatting issues
|
||||||
|
- [ ] Unused imports/variables
|
||||||
|
- [ ] Any type usage
|
||||||
|
- [ ] Console.log statements
|
||||||
|
- [ ] Missing return types
|
||||||
|
|
||||||
|
### Pre-merge Blockers
|
||||||
|
- [ ] All pre-commit checks pass
|
||||||
|
- [ ] Code review approval (2+ reviewers)
|
||||||
|
- [ ] Security scan clean
|
||||||
|
- [ ] Performance tests pass
|
||||||
|
- [ ] Documentation updated
|
||||||
|
- [ ] Breaking changes documented
|
||||||
|
- [ ] Migration scripts included (if needed)
|
||||||
|
|
||||||
|
## 🚨 Red Flags - Immediate Action Required
|
||||||
|
|
||||||
|
### Code Smells That Must Be Fixed
|
||||||
|
1. **Any type usage** - Refactor to proper types
|
||||||
|
2. **Console.log statements** - Replace with Logger service
|
||||||
|
3. **Try-catch without specific error handling** - Handle specific errors
|
||||||
|
4. **Functions over 50 lines** - Break into smaller functions
|
||||||
|
5. **Deep nesting** - Refactor with early returns
|
||||||
|
6. **Duplicate code** - Extract to shared utilities
|
||||||
|
7. **Magic numbers/strings** - Create named constants
|
||||||
|
8. **Missing error handling** - Add proper error boundaries
|
||||||
|
9. **Synchronous database calls** - Use async/await
|
||||||
|
10. **Missing input validation** - Add DTOs and validators
|
||||||
|
|
||||||
|
### Security Red Flags
|
||||||
|
- Unvalidated user input
|
||||||
|
- SQL injection vulnerabilities
|
||||||
|
- Missing authentication checks
|
||||||
|
- Exposed sensitive data
|
||||||
|
- Weak password policies
|
||||||
|
- Missing CSRF protection
|
||||||
|
- Insecure direct object references
|
||||||
|
|
||||||
|
## 📝 Naming Memory Aids
|
||||||
|
|
||||||
|
### Variable Naming Patterns
|
||||||
|
```typescript
|
||||||
|
// Boolean flags
|
||||||
|
const isUserActive = true;
|
||||||
|
const hasPermission = false;
|
||||||
|
const shouldValidate = true;
|
||||||
|
|
||||||
|
// Collections
|
||||||
|
const userList = [];
|
||||||
|
const activeUsers = [];
|
||||||
|
const userEmails = [];
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
const getUserById = () => {};
|
||||||
|
const validateEmail = () => {};
|
||||||
|
const calculateTotal = () => {};
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const MAX_LOGIN_ATTEMPTS = 3;
|
||||||
|
const API_BASE_URL = 'https://api.example.com';
|
||||||
|
const DEFAULT_PAGE_SIZE = 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Class Naming Patterns
|
||||||
|
```typescript
|
||||||
|
// Services
|
||||||
|
class UserService {}
|
||||||
|
class EmailService {}
|
||||||
|
class ValidationService {}
|
||||||
|
|
||||||
|
// Controllers
|
||||||
|
class UserController {}
|
||||||
|
class AuthController {}
|
||||||
|
class PostController {}
|
||||||
|
|
||||||
|
// DTOs
|
||||||
|
class CreateUserDto {}
|
||||||
|
class UpdateUserDto {}
|
||||||
|
class UserResponseDto {}
|
||||||
|
|
||||||
|
// Entities
|
||||||
|
class User {}
|
||||||
|
class Post {}
|
||||||
|
class Comment {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Common Patterns to Remember
|
||||||
|
|
||||||
|
### Error Handling Pattern
|
||||||
|
```typescript
|
||||||
|
// Always use specific error types
|
||||||
|
try {
|
||||||
|
const result = await someOperation();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
throw new BadRequestException(error.message);
|
||||||
|
}
|
||||||
|
if (error instanceof NotFoundError) {
|
||||||
|
throw new NotFoundException(error.message);
|
||||||
|
}
|
||||||
|
throw new InternalServerErrorException('Unexpected error occurred');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async Function Pattern
|
||||||
|
```typescript
|
||||||
|
// Always explicit return types for async functions
|
||||||
|
const fetchUserData = async (userId: string): Promise<UserData> => {
|
||||||
|
const user = await this.userRepository.findById(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new UserNotFoundException(userId);
|
||||||
|
}
|
||||||
|
return UserData.fromEntity(user);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation Pattern
|
||||||
|
```typescript
|
||||||
|
// Always validate input with DTOs
|
||||||
|
@Post()
|
||||||
|
async createUser(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
|
||||||
|
// DTO validation happens automatically
|
||||||
|
const user = await this.userService.create(createUserDto);
|
||||||
|
return UserResponseDto.fromEntity(user);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Repository Pattern
|
||||||
|
```typescript
|
||||||
|
// Always use interfaces for repositories
|
||||||
|
interface IUserRepository {
|
||||||
|
findById(id: string): Promise<User | null>;
|
||||||
|
save(user: User): Promise<User>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserRepository implements IUserRepository {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing Patterns to Remember
|
||||||
|
|
||||||
|
### Unit Test Structure
|
||||||
|
```typescript
|
||||||
|
describe('UserService', () => {
|
||||||
|
let service: UserService;
|
||||||
|
let repository: IUserRepository;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Setup
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('methodName', () => {
|
||||||
|
it('should do something when condition', async () => {
|
||||||
|
// Arrange
|
||||||
|
// Act
|
||||||
|
// Assert
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when invalid input', async () => {
|
||||||
|
// Test error cases
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Data Patterns
|
||||||
|
```typescript
|
||||||
|
// Create test data factories
|
||||||
|
const createMockUser = (overrides: Partial<User> = {}): User => ({
|
||||||
|
id: '1',
|
||||||
|
email: 'test@example.com',
|
||||||
|
name: 'Test User',
|
||||||
|
createdAt: new Date(),
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createMockCreateUserDto = (overrides: Partial<CreateUserDto> = {}): CreateUserDto => ({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
name: 'Test User',
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Development Workflow Memory
|
||||||
|
|
||||||
|
### Daily Checklist
|
||||||
|
- [ ] Run `npm run lint:strict` before starting work
|
||||||
|
- [ ] Run `npm run type-check` after major changes
|
||||||
|
- [ ] Write tests for new functionality
|
||||||
|
- [ ] Update documentation if needed
|
||||||
|
- [ ] Check for security implications
|
||||||
|
- [ ] Consider performance impact
|
||||||
|
|
||||||
|
### Before Commit Checklist
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] No ESLint errors
|
||||||
|
- [ ] No TypeScript errors
|
||||||
|
- [ ] Code is formatted
|
||||||
|
- [ ] No console.log statements
|
||||||
|
- [ ] No any types
|
||||||
|
- [ ] Proper error handling
|
||||||
|
- [ ] Input validation in place
|
||||||
|
|
||||||
|
### Code Review Checklist
|
||||||
|
- [ ] Follows naming conventions
|
||||||
|
- [ ] Proper error handling
|
||||||
|
- [ ] Security considerations
|
||||||
|
- [ ] Performance implications
|
||||||
|
- [ ] Test coverage adequate
|
||||||
|
- [ ] Documentation updated
|
||||||
|
- [ ] No code duplication
|
||||||
|
|
||||||
|
## 🚀 Performance Memory Aids
|
||||||
|
|
||||||
|
### Database Optimization
|
||||||
|
- Use pagination for list endpoints
|
||||||
|
- Implement proper indexing
|
||||||
|
- Use database transactions when needed
|
||||||
|
- Avoid N+1 queries
|
||||||
|
- Use connection pooling
|
||||||
|
|
||||||
|
### Caching Strategy
|
||||||
|
```typescript
|
||||||
|
// Cache frequently accessed data
|
||||||
|
@Cacheable('user', 300) // 5 minutes
|
||||||
|
async findOne(id: string): Promise<User> {
|
||||||
|
return this.userRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate cache on updates
|
||||||
|
@CacheEvict('user', { allEntries: true })
|
||||||
|
async update(id: string, data: UpdateUserDto): Promise<User> {
|
||||||
|
// Update logic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory Management
|
||||||
|
- Dispose of resources properly
|
||||||
|
- Avoid memory leaks
|
||||||
|
- Use streaming for large data
|
||||||
|
- Implement proper cleanup in services
|
||||||
|
|
||||||
|
## 🔒 Security Memory Aids
|
||||||
|
|
||||||
|
### Authentication Checklist
|
||||||
|
- [ ] JWT tokens properly validated
|
||||||
|
- [ ] Password hashing with bcrypt
|
||||||
|
- [ ] Rate limiting implemented
|
||||||
|
- [ ] CORS properly configured
|
||||||
|
- [ ] Helmet security headers
|
||||||
|
|
||||||
|
### Input Validation Checklist
|
||||||
|
- [ ] All inputs validated with DTOs
|
||||||
|
- [ ] SQL injection prevention
|
||||||
|
- [ ] XSS protection
|
||||||
|
- [ ] File upload validation
|
||||||
|
- [ ] Size limits enforced
|
||||||
|
|
||||||
|
### Authorization Patterns
|
||||||
|
```typescript
|
||||||
|
// Use guards and decorators
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('admin')
|
||||||
|
@Get('admin-only')
|
||||||
|
async adminEndpoint(): Promise<string> {
|
||||||
|
return 'Admin only content';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Monitoring and Alerting
|
||||||
|
|
||||||
|
### Key Metrics to Track
|
||||||
|
- Response times
|
||||||
|
- Error rates
|
||||||
|
- Memory usage
|
||||||
|
- Database query performance
|
||||||
|
- API usage patterns
|
||||||
|
- Security events
|
||||||
|
|
||||||
|
### Logging Standards
|
||||||
|
```typescript
|
||||||
|
// Use structured logging
|
||||||
|
this.logger.log(`User ${userId} created successfully`, {
|
||||||
|
userId,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
action: 'user_created',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Refactoring Memory Aids
|
||||||
|
|
||||||
|
### When to Refactor
|
||||||
|
- Functions over 50 lines
|
||||||
|
- Deep nesting (>3 levels)
|
||||||
|
- Code duplication
|
||||||
|
- Complex conditionals
|
||||||
|
- Poor naming
|
||||||
|
- Missing error handling
|
||||||
|
|
||||||
|
### Refactoring Techniques
|
||||||
|
- Extract method
|
||||||
|
- Extract class
|
||||||
|
- Replace magic numbers with constants
|
||||||
|
- Use early returns
|
||||||
|
- Replace loops with functional methods
|
||||||
|
- Extract interfaces
|
||||||
|
|
||||||
|
## 🎓 Learning Resources
|
||||||
|
|
||||||
|
### TypeScript Best Practices
|
||||||
|
- Use strict mode always
|
||||||
|
- Prefer interfaces over types for object shapes
|
||||||
|
- Use type guards for runtime type checking
|
||||||
|
- Leverage utility types (Partial, Pick, Omit)
|
||||||
|
- Use generic types for reusable code
|
||||||
|
|
||||||
|
### NestJS Best Practices
|
||||||
|
- Use dependency injection
|
||||||
|
- Implement proper error handling
|
||||||
|
- Use guards for authentication/authorization
|
||||||
|
- Use interceptors for cross-cutting concerns
|
||||||
|
- Use pipes for validation and transformation
|
||||||
|
|
||||||
|
### Functional Programming
|
||||||
|
- Pure functions (no side effects)
|
||||||
|
- Immutable data structures
|
||||||
|
- Higher-order functions
|
||||||
|
- Function composition
|
||||||
|
- Avoid mutations
|
||||||
|
|
||||||
|
## 🆘 Emergency Procedures
|
||||||
|
|
||||||
|
### When Rules Need to Be Bypassed
|
||||||
|
1. Document the exception in `EXCEPTIONS.md`
|
||||||
|
2. Get team approval via PR review
|
||||||
|
3. Add specific ESLint disable comment with justification
|
||||||
|
4. Set a deadline for proper fix
|
||||||
|
5. Monitor for abuse
|
||||||
|
|
||||||
|
### Emergency Override Commands
|
||||||
|
```bash
|
||||||
|
# Skip pre-commit hooks (DANGEROUS - use only in emergencies)
|
||||||
|
git commit --no-verify -m "emergency fix: [description]"
|
||||||
|
|
||||||
|
# Temporarily disable ESLint rule
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const data: any = externalApiResponse;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Quick Reference Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check code quality
|
||||||
|
npm run lint:strict
|
||||||
|
npm run type-check
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Fix auto-fixable issues
|
||||||
|
npm run lint:strict:fix
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
# Run specific checks
|
||||||
|
npm run test:unit
|
||||||
|
npm run test:integration
|
||||||
|
npm run test:e2e
|
||||||
|
npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember**: These rules exist to create maintainable, scalable, and secure applications. When in doubt, choose the more explicit, type-safe, and functional approach.
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
26
.eslintrc.js
Normal file
26
.eslintrc.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
159
.gitignore
vendored
Normal file
159
.gitignore
vendored
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# temp
|
||||||
|
.tmp
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
13
.npmrc
Normal file
13
.npmrc
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# NPM Configuration for NestJS Serverless Project
|
||||||
|
audit-level=moderate
|
||||||
|
fund=false
|
||||||
|
legacy-peer-deps=false
|
||||||
|
save-exact=false
|
||||||
|
engine-strict=true
|
||||||
|
|
||||||
|
# Suppress warnings for transitive dependencies
|
||||||
|
loglevel=error
|
||||||
|
|
||||||
|
# Use npm ci for faster, reliable installs
|
||||||
|
prefer-offline=true
|
||||||
|
cache-min=86400
|
||||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
|
|
||||||
62
Dockerfile
Normal file
62
Dockerfile
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Multi-stage build for NestJS Serverless Application
|
||||||
|
FROM node:18-alpine AS builder
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY prisma ./prisma/
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci --only=production && npm cache clean --force
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Generate Prisma client
|
||||||
|
RUN npx prisma generate
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:18-alpine AS production
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install serverless framework globally
|
||||||
|
RUN npm install -g serverless
|
||||||
|
|
||||||
|
# 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/dist ./dist
|
||||||
|
COPY --from=builder /app/prisma ./prisma
|
||||||
|
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
|
||||||
|
|
||||||
|
# Copy serverless configuration
|
||||||
|
COPY serverless*.yml ./
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1001 -S nodejs
|
||||||
|
RUN adduser -S nestjs -u 1001
|
||||||
|
|
||||||
|
# Change ownership
|
||||||
|
RUN chown -R nestjs:nodejs /app
|
||||||
|
USER nestjs
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:3000/health || exit 1
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["npm", "run", "start:prod"]
|
||||||
328
README.md
Normal file
328
README.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# NestJS Serverless User CRUD Application
|
||||||
|
|
||||||
|
A professional serverless NestJS application with AWS Lambda, user CRUD operations, authentication, and comprehensive CI/CD pipeline.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ☁️ **Serverless Architecture** - AWS Lambda with API Gateway
|
||||||
|
- 🔐 **Authentication & Authorization** - JWT-based authentication with role-based access control
|
||||||
|
- 👥 **User Management** - Complete CRUD operations for users
|
||||||
|
- 📝 **Post Management** - Create, read, update, and delete posts
|
||||||
|
- 🏗️ **Clean Architecture** - MVC + Clean Architecture patterns
|
||||||
|
- 🗄️ **Database** - PostgreSQL with Prisma ORM (RDS)
|
||||||
|
- 📚 **API Documentation** - Swagger/OpenAPI documentation
|
||||||
|
- 🛡️ **Security** - Helmet, CORS, rate limiting, input validation, WAF
|
||||||
|
- 🧪 **Testing Ready** - Jest configuration with CI/CD
|
||||||
|
- 🚀 **CI/CD Pipeline** - GitHub Actions with automated deployment
|
||||||
|
- 🌍 **Multi-Environment** - Dev, Staging, and Production environments
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Framework**: NestJS
|
||||||
|
- **Runtime**: AWS Lambda (Node.js 18.x)
|
||||||
|
- **API Gateway**: AWS API Gateway
|
||||||
|
- **Database**: PostgreSQL (AWS RDS)
|
||||||
|
- **ORM**: Prisma
|
||||||
|
- **Authentication**: JWT with Passport
|
||||||
|
- **Validation**: class-validator & class-transformer
|
||||||
|
- **Documentation**: Swagger/OpenAPI
|
||||||
|
- **Security**: Helmet, bcryptjs, AWS WAF
|
||||||
|
- **Deployment**: Serverless Framework
|
||||||
|
- **CI/CD**: GitHub Actions
|
||||||
|
- **Infrastructure**: AWS (Lambda, RDS, VPC, CloudWatch)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── common/ # Shared utilities and configurations
|
||||||
|
│ ├── database/ # Database configuration
|
||||||
|
│ ├── decorators/ # Custom decorators
|
||||||
|
│ ├── dto/ # Common DTOs
|
||||||
|
│ ├── guards/ # Custom guards
|
||||||
|
│ ├── interfaces/ # TypeScript interfaces
|
||||||
|
│ └── utils/ # Utility functions
|
||||||
|
├── modules/ # Feature modules
|
||||||
|
│ ├── auth/ # Authentication module
|
||||||
|
│ │ ├── controllers/ # Auth controllers
|
||||||
|
│ │ ├── dto/ # Auth DTOs
|
||||||
|
│ │ ├── guards/ # Auth guards
|
||||||
|
│ │ ├── services/ # Auth services
|
||||||
|
│ │ └── strategies/ # Passport strategies
|
||||||
|
│ ├── users/ # Users module
|
||||||
|
│ │ ├── controllers/ # User controllers
|
||||||
|
│ │ ├── dto/ # User DTOs
|
||||||
|
│ │ └── services/ # User services
|
||||||
|
│ └── posts/ # Posts module
|
||||||
|
│ ├── controllers/ # Post controllers
|
||||||
|
│ ├── dto/ # Post DTOs
|
||||||
|
│ └── services/ # Post services
|
||||||
|
├── app.module.ts # Root module
|
||||||
|
├── app.controller.ts # Root controller
|
||||||
|
├── app.service.ts # Root service
|
||||||
|
└── main.ts # Application entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js (v18 or higher)
|
||||||
|
- AWS CLI configured
|
||||||
|
- PostgreSQL database (local or AWS RDS)
|
||||||
|
- npm or yarn
|
||||||
|
- Serverless Framework
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd nestjs-serverless-user-crud
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Set up environment variables**
|
||||||
|
```bash
|
||||||
|
# Copy environment template
|
||||||
|
cp env.dev .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the `.env` file with your database credentials and JWT secret.
|
||||||
|
|
||||||
|
4. **Set up AWS resources**
|
||||||
|
```bash
|
||||||
|
# Run the AWS setup script
|
||||||
|
chmod +x scripts/setup-aws.sh
|
||||||
|
./scripts/setup-aws.sh dev
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Set up the database**
|
||||||
|
```bash
|
||||||
|
# Generate Prisma client
|
||||||
|
npm run prisma:generate
|
||||||
|
|
||||||
|
# Run database migrations
|
||||||
|
npm run prisma:migrate
|
||||||
|
|
||||||
|
# Seed the database (optional)
|
||||||
|
npm run prisma:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Start the application**
|
||||||
|
```bash
|
||||||
|
# Local development
|
||||||
|
npm run start:dev
|
||||||
|
|
||||||
|
# Serverless offline
|
||||||
|
npm run start:serverless
|
||||||
|
|
||||||
|
# Deploy to AWS
|
||||||
|
npm run deploy:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
Once the application is running, you can access the Swagger documentation at:
|
||||||
|
- **Local Development**: http://localhost:3000/api/docs
|
||||||
|
- **Serverless Offline**: http://localhost:3000/api/docs
|
||||||
|
- **AWS Lambda**: https://your-api-gateway-url/api/docs (dev environment only)
|
||||||
|
|
||||||
|
## Serverless Deployment
|
||||||
|
|
||||||
|
### Environment Configuration
|
||||||
|
|
||||||
|
The application supports multiple environments:
|
||||||
|
- **Development** (`dev`) - For local development and testing
|
||||||
|
- **Staging** (`staging`) - For pre-production testing
|
||||||
|
- **Production** (`prod`) - For live production environment
|
||||||
|
|
||||||
|
### Deployment Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy to development
|
||||||
|
npm run deploy:dev
|
||||||
|
|
||||||
|
# Deploy to staging
|
||||||
|
npm run deploy:staging
|
||||||
|
|
||||||
|
# Deploy to production
|
||||||
|
npm run deploy:prod
|
||||||
|
|
||||||
|
# Remove deployment
|
||||||
|
npm run remove:dev
|
||||||
|
npm run remove:staging
|
||||||
|
npm run remove:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Set up the following environment variables for each environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
DATABASE_URL="postgresql://username:password@host:5432/database?schema=public"
|
||||||
|
|
||||||
|
# JWT Configuration
|
||||||
|
JWT_SECRET="your-super-secret-jwt-key"
|
||||||
|
JWT_EXPIRES_IN="7d"
|
||||||
|
|
||||||
|
# AWS Configuration
|
||||||
|
AWS_REGION="us-east-1"
|
||||||
|
VPC_SECURITY_GROUP_ID="sg-xxxxxxxxx"
|
||||||
|
VPC_SUBNET_ID_1="subnet-xxxxxxxxx"
|
||||||
|
VPC_SUBNET_ID_2="subnet-yyyyyyyyy"
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- `POST /api/v1/auth/register` - Register a new user
|
||||||
|
- `POST /api/v1/auth/login` - Login user
|
||||||
|
- `POST /api/v1/auth/refresh` - Refresh access token
|
||||||
|
|
||||||
|
### Users
|
||||||
|
- `GET /api/v1/users` - Get all users (Admin/Moderator only)
|
||||||
|
- `GET /api/v1/users/profile` - Get current user profile
|
||||||
|
- `GET /api/v1/users/:id` - Get user by ID (Admin/Moderator only)
|
||||||
|
- `PATCH /api/v1/users/profile` - Update current user profile
|
||||||
|
- `PATCH /api/v1/users/:id` - Update user by ID (Admin only)
|
||||||
|
- `DELETE /api/v1/users/:id` - Delete user by ID (Admin only)
|
||||||
|
|
||||||
|
### Posts
|
||||||
|
- `GET /api/v1/posts` - Get all posts
|
||||||
|
- `GET /api/v1/posts/my-posts` - Get current user's posts
|
||||||
|
- `GET /api/v1/posts/:id` - Get post by ID
|
||||||
|
- `POST /api/v1/posts` - Create a new post
|
||||||
|
- `PATCH /api/v1/posts/:id` - Update post by ID
|
||||||
|
- `DELETE /api/v1/posts/:id` - Delete post by ID
|
||||||
|
|
||||||
|
## User Roles
|
||||||
|
|
||||||
|
- **USER**: Basic user with limited permissions
|
||||||
|
- **MODERATOR**: Can view all users and moderate content
|
||||||
|
- **ADMIN**: Full access to all resources
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### Users Table
|
||||||
|
- `id` - Unique identifier
|
||||||
|
- `email` - User email (unique)
|
||||||
|
- `username` - Username (unique)
|
||||||
|
- `password` - Hashed password
|
||||||
|
- `firstName` - First name
|
||||||
|
- `lastName` - Last name
|
||||||
|
- `isActive` - Account status
|
||||||
|
- `role` - User role (USER, MODERATOR, ADMIN)
|
||||||
|
- `createdAt` - Creation timestamp
|
||||||
|
- `updatedAt` - Last update timestamp
|
||||||
|
|
||||||
|
### Posts Table
|
||||||
|
- `id` - Unique identifier
|
||||||
|
- `title` - Post title
|
||||||
|
- `content` - Post content
|
||||||
|
- `published` - Publication status
|
||||||
|
- `authorId` - Foreign key to users table
|
||||||
|
- `createdAt` - Creation timestamp
|
||||||
|
- `updatedAt` - Last update timestamp
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
- **Password Hashing**: bcryptjs for secure password storage
|
||||||
|
- **JWT Authentication**: Secure token-based authentication
|
||||||
|
- **Role-based Access Control**: Different permissions for different roles
|
||||||
|
- **Input Validation**: Comprehensive validation using class-validator
|
||||||
|
- **Rate Limiting**: Protection against brute force attacks
|
||||||
|
- **CORS**: Configurable cross-origin resource sharing
|
||||||
|
- **Helmet**: Security headers for HTTP responses
|
||||||
|
|
||||||
|
## CI/CD Pipeline
|
||||||
|
|
||||||
|
The application includes a comprehensive CI/CD pipeline using GitHub Actions:
|
||||||
|
|
||||||
|
### Pipeline Stages
|
||||||
|
|
||||||
|
1. **Test & Lint** - Runs unit tests, e2e tests, and code linting
|
||||||
|
2. **Build** - Builds the application and uploads artifacts
|
||||||
|
3. **Deploy Dev** - Deploys to development environment (on develop branch)
|
||||||
|
4. **Deploy Staging** - Deploys to staging environment (on main branch)
|
||||||
|
5. **Deploy Production** - Deploys to production environment (manual approval)
|
||||||
|
6. **Security Scan** - Runs security audits and vulnerability scans
|
||||||
|
|
||||||
|
### GitHub Secrets
|
||||||
|
|
||||||
|
Configure the following secrets in your GitHub repository:
|
||||||
|
|
||||||
|
**Development Environment:**
|
||||||
|
- `AWS_ACCESS_KEY_ID_DEV`
|
||||||
|
- `AWS_SECRET_ACCESS_KEY_DEV`
|
||||||
|
- `DATABASE_URL_DEV`
|
||||||
|
- `JWT_SECRET_DEV`
|
||||||
|
- `VPC_SECURITY_GROUP_ID_DEV`
|
||||||
|
- `VPC_SUBNET_ID_1_DEV`
|
||||||
|
- `VPC_SUBNET_ID_2_DEV`
|
||||||
|
|
||||||
|
**Staging Environment:**
|
||||||
|
- `AWS_ACCESS_KEY_ID_STAGING`
|
||||||
|
- `AWS_SECRET_ACCESS_KEY_STAGING`
|
||||||
|
- `DATABASE_URL_STAGING`
|
||||||
|
- `JWT_SECRET_STAGING`
|
||||||
|
- `VPC_SECURITY_GROUP_ID_STAGING`
|
||||||
|
- `VPC_SUBNET_ID_1_STAGING`
|
||||||
|
- `VPC_SUBNET_ID_2_STAGING`
|
||||||
|
|
||||||
|
**Production Environment:**
|
||||||
|
- `AWS_ACCESS_KEY_ID_PROD`
|
||||||
|
- `AWS_SECRET_ACCESS_KEY_PROD`
|
||||||
|
- `DATABASE_URL_PROD`
|
||||||
|
- `JWT_SECRET_PROD`
|
||||||
|
- `VPC_SECURITY_GROUP_ID_PROD`
|
||||||
|
- `VPC_SUBNET_ID_1_PROD`
|
||||||
|
- `VPC_SUBNET_ID_2_PROD`
|
||||||
|
|
||||||
|
**Additional:**
|
||||||
|
- `SNYK_TOKEN` - For security scanning
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Available Scripts
|
||||||
|
|
||||||
|
- `npm run start:dev` - Start in development mode with hot reload
|
||||||
|
- `npm run start:serverless` - Start serverless offline
|
||||||
|
- `npm run build` - Build the application
|
||||||
|
- `npm run build:lambda` - Build for Lambda deployment
|
||||||
|
- `npm run start:prod` - Start in production mode
|
||||||
|
- `npm run test` - Run unit tests
|
||||||
|
- `npm run test:e2e` - Run end-to-end tests
|
||||||
|
- `npm run lint` - Run ESLint
|
||||||
|
- `npm run format` - Format code with Prettier
|
||||||
|
|
||||||
|
### Database Commands
|
||||||
|
|
||||||
|
- `npm run prisma:generate` - Generate Prisma client
|
||||||
|
- `npm run prisma:push` - Push schema changes to database
|
||||||
|
- `npm run prisma:migrate` - Run database migrations
|
||||||
|
- `npm run prisma:studio` - Open Prisma Studio
|
||||||
|
- `npm run prisma:seed` - Seed the database
|
||||||
|
|
||||||
|
### Deployment Scripts
|
||||||
|
|
||||||
|
- `./scripts/deploy.sh [environment]` - Deploy to specified environment
|
||||||
|
- `./scripts/setup-aws.sh [environment]` - Set up AWS resources
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Add tests if applicable
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License.
|
||||||
|
|
||||||
86
docker-compose.yml
Normal file
86
docker-compose.yml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# PostgreSQL Database
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: nestjs-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: nestjs_user_crud
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
networks:
|
||||||
|
- nestjs-network
|
||||||
|
|
||||||
|
# Redis for caching (optional)
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: nestjs-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- nestjs-network
|
||||||
|
|
||||||
|
# NestJS Application
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: production
|
||||||
|
container_name: nestjs-app
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/nestjs_user_crud?schema=public
|
||||||
|
JWT_SECRET: docker-jwt-secret-key
|
||||||
|
JWT_EXPIRES_IN: 7d
|
||||||
|
API_PREFIX: api/v1
|
||||||
|
API_VERSION: 1.0.0
|
||||||
|
THROTTLE_TTL: 60
|
||||||
|
THROTTLE_LIMIT: 10
|
||||||
|
CORS_ORIGIN: http://localhost:3000
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- nestjs-network
|
||||||
|
volumes:
|
||||||
|
- ./src:/app/src
|
||||||
|
- ./prisma:/app/prisma
|
||||||
|
|
||||||
|
# Prisma Studio
|
||||||
|
prisma-studio:
|
||||||
|
image: node:18-alpine
|
||||||
|
container_name: nestjs-prisma-studio
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5555:5555"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/nestjs_user_crud?schema=public
|
||||||
|
working_dir: /app
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
command: sh -c "npm install && npx prisma generate && npx prisma studio --hostname 0.0.0.0"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- nestjs-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nestjs-network:
|
||||||
|
driver: bridge
|
||||||
19
init.sql
Normal file
19
init.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
-- Initialize database for NestJS Serverless Application
|
||||||
|
-- This file is executed when the PostgreSQL container starts
|
||||||
|
|
||||||
|
-- Create database if it doesn't exist
|
||||||
|
SELECT 'CREATE DATABASE nestjs_user_crud'
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'nestjs_user_crud')\gexec
|
||||||
|
|
||||||
|
-- Connect to the database
|
||||||
|
\c nestjs_user_crud;
|
||||||
|
|
||||||
|
-- Create extensions if needed
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- Set timezone
|
||||||
|
SET timezone = 'UTC';
|
||||||
|
|
||||||
|
-- Create a user for the application (optional)
|
||||||
|
-- CREATE USER nestjs_user WITH PASSWORD 'nestjs_password';
|
||||||
|
-- GRANT ALL PRIVILEGES ON DATABASE nestjs_user_crud TO nestjs_user;
|
||||||
9
nest-cli.json
Normal file
9
nest-cli.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
9657
package-lock.json
generated
Normal file
9657
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
97
package.json
Normal file
97
package.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"name": "nestjs-user-crud",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "NestJS application with user CRUD and authentication",
|
||||||
|
"author": "Your Name",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"prisma:generate": "prisma generate",
|
||||||
|
"prisma:push": "prisma db push",
|
||||||
|
"prisma:migrate": "prisma migrate dev",
|
||||||
|
"prisma:studio": "prisma studio",
|
||||||
|
"prisma:seed": "ts-node prisma/seed.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^10.3.0",
|
||||||
|
"@nestjs/config": "^3.1.1",
|
||||||
|
"@nestjs/core": "^10.3.0",
|
||||||
|
"@nestjs/jwt": "^10.2.0",
|
||||||
|
"@nestjs/passport": "^10.0.3",
|
||||||
|
"@nestjs/platform-express": "^10.3.0",
|
||||||
|
"@nestjs/swagger": "^7.1.17",
|
||||||
|
"@nestjs/throttler": "^5.1.1",
|
||||||
|
"@prisma/client": "^5.8.1",
|
||||||
|
"@types/http-status": "^1.1.2",
|
||||||
|
"aws-lambda": "^1.0.7",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.0",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"http-status": "^2.1.0",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"serverless": "^4.23.0",
|
||||||
|
"swagger-ui-express": "^5.0.0",
|
||||||
|
"yup": "^1.7.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.3.0",
|
||||||
|
"@nestjs/schematics": "^10.1.0",
|
||||||
|
"@nestjs/testing": "^10.3.0",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/node": "^20.11.16",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"@types/passport-local": "^1.0.38",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-prettier": "^8.10.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"prisma": "^5.8.1",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^6.3.4",
|
||||||
|
"ts-jest": "^29.1.2",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
1408
prisma/schema.prisma
Normal file
1408
prisma/schema.prisma
Normal file
File diff suppressed because it is too large
Load Diff
79
serverless.yml
Normal file
79
serverless.yml
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#org: WDI
|
||||||
|
service: minglarDev
|
||||||
|
|
||||||
|
# plugins:
|
||||||
|
# - serverless-domain-manager
|
||||||
|
# custom:
|
||||||
|
# customDomain:
|
||||||
|
# domainName: sandbox.tanamicapital.com
|
||||||
|
# basePath: ''
|
||||||
|
# stage: dev
|
||||||
|
# createRoute53Record: true
|
||||||
|
# endpointType: 'edge'
|
||||||
|
provider:
|
||||||
|
name: aws
|
||||||
|
versionFunctions: false
|
||||||
|
runtime: nodejs20.x
|
||||||
|
region: me-south-1
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: ${env:DATABASE_URL}
|
||||||
|
BY_PASS_EMAIL: ${env:BY_PASS_EMAIL}
|
||||||
|
BYPASS_OTP: ${env:BYPASS_OTP}
|
||||||
|
ENCRYPTION_KEY: ${env:ENCRYPTION_KEY}
|
||||||
|
BREVO_EMAIL_API_KEY: ${env:BREVO_EMAIL_API_KEY}
|
||||||
|
BREVO_API_BASEURL: ${env:BREVO_API_BASEURL}
|
||||||
|
BREVO_FROM_EMAIL: ${env:BREVO_FROM_EMAIL}
|
||||||
|
BREVO_SMTP_HOST: ${env:BREVO_SMTP_HOST}
|
||||||
|
BREVO_SMTP_PORT: ${env:BREVO_SMTP_PORT}
|
||||||
|
BREVO_SMTP_USER: ${env:BREVO_SMTP_USER}
|
||||||
|
BREVO_SMTP_PASS: ${env:BREVO_SMTP_PASS}
|
||||||
|
TWILIO_ACCOUNT_SID: ${env:TWILIO_ACCOUNT_SID}
|
||||||
|
TWILIO_AUTH_TOKEN: ${env:TWILIO_AUTH_TOKEN}
|
||||||
|
TWILIO_SMS_FROM: ${env:TWILIO_SMS_FROM}
|
||||||
|
TWILIO_ALPHA_SENDER: ${env:TWILIO_ALPHA_SENDER}
|
||||||
|
REFRESH_TOKEN_SECRET: ${env:REFRESH_TOKEN_SECRET}
|
||||||
|
JWT_SECRET: ${env:JWT_SECRET}
|
||||||
|
API_URL: ${env:API_URL}
|
||||||
|
COGNITO_USER_POOL_ID: ${env:COGNITO_USER_POOL_ID}
|
||||||
|
COGNITO_CLIENT_ID: ${env:COGNITO_CLIENT_ID}
|
||||||
|
COGNITO_REGION: ${env:COGNITO_REGION}
|
||||||
|
DEFAULT_PASSWORD: ${env:DEFAULT_PASSWORD}
|
||||||
|
SALT_ROUNDS: ${env:SALT_ROUNDS}
|
||||||
|
TEST_NUMBERS: ${env:TEST_NUMBERS}
|
||||||
|
DEFAULT_OTP: ${env:DEFAULT_OTP}
|
||||||
|
AWS_S3_BUCKET: ${env:AWS_S3_BUCKET}
|
||||||
|
|
||||||
|
iamRoleStatements:
|
||||||
|
- Effect: Allow
|
||||||
|
Action:
|
||||||
|
- cognito-idp:AdminUpdateUserAttributes
|
||||||
|
Resource: arn:aws:cognito-idp:${env:COGNITO_REGION}:058264420740:userpool/${env:COGNITO_USER_POOL_ID}
|
||||||
|
|
||||||
|
httpApi:
|
||||||
|
cors:
|
||||||
|
allowedOrigins:
|
||||||
|
- '*'
|
||||||
|
allowedHeaders:
|
||||||
|
- Content-Type
|
||||||
|
- X-Amz-Date
|
||||||
|
- Authorization
|
||||||
|
- X-Api-Key
|
||||||
|
allowCredentials: false
|
||||||
|
|
||||||
|
package:
|
||||||
|
individually: true
|
||||||
|
patterns:
|
||||||
|
- '!**/*' # Exclude everything by default
|
||||||
|
- 'src/lambda/**' # Include handler files
|
||||||
|
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' # Specific engine file
|
||||||
|
- 'node_modules/@prisma/client/**' # Prisma client
|
||||||
|
- '!node_modules/**' # Exclude other node_modules
|
||||||
|
- '!node_modules/.prisma/client/query-engine-*' # Exclude other engines
|
||||||
|
|
||||||
|
functions:
|
||||||
|
testFunc:
|
||||||
|
handler: src/lambda/testFunc.handler
|
||||||
|
events:
|
||||||
|
- httpApi:
|
||||||
|
path: /testFunc
|
||||||
|
method: get
|
||||||
45
src/app/app.module.ts
Normal file
45
src/app/app.module.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
// Common imports
|
||||||
|
import { PrismaModule } from '../common/database/prisma.module';
|
||||||
|
import { RolesGuard } from '../common/guards/roles.guard';
|
||||||
|
|
||||||
|
// Feature modules
|
||||||
|
// import { AuthModule } from '../modules/auth/auth.module';
|
||||||
|
// import { HostModule } from '../modules/host/host.module'; // Add more modules as you create them
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
// Global configuration (env variables)
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
envFilePath: '.env',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// JWT for authentication
|
||||||
|
JwtModule.register({
|
||||||
|
global: true,
|
||||||
|
secret: process.env.JWT_SECRET || 'default_secret',
|
||||||
|
signOptions: { expiresIn: '1d' },
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Database
|
||||||
|
PrismaModule,
|
||||||
|
|
||||||
|
// App modules
|
||||||
|
// AuthModule,
|
||||||
|
// HostModule,
|
||||||
|
],
|
||||||
|
|
||||||
|
providers: [
|
||||||
|
// Global guards
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: RolesGuard,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
10
src/common/database/prisma.module.ts
Normal file
10
src/common/database/prisma.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService],
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
|
|
||||||
31
src/common/database/prisma.service.ts
Normal file
31
src/common/database/prisma.service.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable, OnModuleInit, OnModuleDestroy, INestApplication } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
datasources: {
|
||||||
|
db: {
|
||||||
|
url: process.env.DATABASE_URL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.$disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async enableShutdownHooks(app: INestApplication) {
|
||||||
|
process.on('beforeExit', async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
12
src/common/decorators/current-user.decorator.ts
Normal file
12
src/common/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { User } from '@prisma/client';
|
||||||
|
|
||||||
|
export const CurrentUser = createParamDecorator(
|
||||||
|
(data: keyof User | undefined, ctx: ExecutionContext): User | any => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
const user = request.user;
|
||||||
|
|
||||||
|
return data ? user?.[data] : user;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
5
src/common/decorators/roles.decorator.ts
Normal file
5
src/common/decorators/roles.decorator.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const ROLES_KEY = 'roles';
|
||||||
|
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||||
|
|
||||||
55
src/common/dto/pagination.dto.ts
Normal file
55
src/common/dto/pagination.dto.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { IsOptional, IsPositive, Min, Max } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class PaginationDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Page number',
|
||||||
|
minimum: 1,
|
||||||
|
default: 1,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsPositive()
|
||||||
|
@Min(1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Number of items per page',
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 100,
|
||||||
|
default: 10,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsPositive()
|
||||||
|
@Min(1)
|
||||||
|
@Max(100)
|
||||||
|
limit?: number = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PaginationMetaDto {
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
page: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
limit: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
totalPages: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
hasNext: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
hasPrev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PaginatedResponseDto<T> {
|
||||||
|
data: T[];
|
||||||
|
meta: PaginationMetaDto;
|
||||||
|
}
|
||||||
|
|
||||||
25
src/common/guards/roles.guard.ts
Normal file
25
src/common/guards/roles.guard.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { ROLES_KEY } from '../decorators/roles.decorator';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
constructor(private reflector: Reflector) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!requiredRoles) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
// For now, allow all requests since we don't have role-based auth implemented
|
||||||
|
// This should be updated when you implement proper role checking
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/common/interfaces/auth.interface.ts
Normal file
19
src/common/interfaces/auth.interface.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { User } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface JwtPayload {
|
||||||
|
sub: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
user: Omit<User, 'password'>;
|
||||||
|
accessToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
103
src/common/middlewares/jwt/authForHost.ts
Normal file
103
src/common/middlewares/jwt/authForHost.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import httpStatus from 'http-status';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import ApiError from '../../utils/helper/ApiError';
|
||||||
|
import config from '../../../config/config';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
interface DecodedToken {
|
||||||
|
id: number;
|
||||||
|
role?: string;
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserPayload {
|
||||||
|
id: string;
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'express-serve-static-core' {
|
||||||
|
interface Request {
|
||||||
|
user?: UserPayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies JWT and validates Host user (role_xid = 3)
|
||||||
|
*/
|
||||||
|
const verifyCallback = async (
|
||||||
|
req: Request,
|
||||||
|
resolve: (value?: unknown) => void,
|
||||||
|
reject: (reason?: Error) => void
|
||||||
|
) => {
|
||||||
|
const token = req.header('x-auth-token') || req.cookies?.accessToken;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, config.jwt.secret) as DecodedToken;
|
||||||
|
|
||||||
|
if (!decoded?.id) {
|
||||||
|
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Fetch user from Prisma (Host user only)
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: decoded.id },
|
||||||
|
include: { role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'User not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Check if user is active
|
||||||
|
if (!user.isActive) {
|
||||||
|
return reject(
|
||||||
|
new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Check Host role (role_xid = 3)
|
||||||
|
if (user.roleXid !== 3) {
|
||||||
|
return reject(
|
||||||
|
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach user to request
|
||||||
|
req.user = { id: user.id.toString(), role: user.role?.roleName };
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof jwt.TokenExpiredError) {
|
||||||
|
return reject(
|
||||||
|
new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reject(
|
||||||
|
new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Express middleware — use as `auth()` in routes
|
||||||
|
*/
|
||||||
|
const auth =
|
||||||
|
() =>
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
verifyCallback(req, resolve, reject);
|
||||||
|
})
|
||||||
|
.then(() => next())
|
||||||
|
.catch((err) => next(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default auth;
|
||||||
103
src/common/middlewares/jwt/authForMinglarAdmin.ts
Normal file
103
src/common/middlewares/jwt/authForMinglarAdmin.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import httpStatus from 'http-status';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import ApiError from '../../utils/helper/ApiError';
|
||||||
|
import config from '../../../config/config';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
interface DecodedToken {
|
||||||
|
id: number;
|
||||||
|
role?: string;
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserPayload {
|
||||||
|
id: string;
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'express-serve-static-core' {
|
||||||
|
interface Request {
|
||||||
|
user?: UserPayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies JWT and validates Host user (role_xid = 3)
|
||||||
|
*/
|
||||||
|
const verifyCallback = async (
|
||||||
|
req: Request,
|
||||||
|
resolve: (value?: unknown) => void,
|
||||||
|
reject: (reason?: Error) => void
|
||||||
|
) => {
|
||||||
|
const token = req.header('x-auth-token') || req.cookies?.accessToken;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, config.jwt.secret) as DecodedToken;
|
||||||
|
|
||||||
|
if (!decoded?.id) {
|
||||||
|
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Fetch user from Prisma (Host user only)
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: decoded.id },
|
||||||
|
include: { role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'User not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Check if user is active
|
||||||
|
if (!user.isActive) {
|
||||||
|
return reject(
|
||||||
|
new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Check Admin role (role_xid = 2)
|
||||||
|
if (user.roleXid !== 2) {
|
||||||
|
return reject(
|
||||||
|
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach user to request
|
||||||
|
req.user = { id: user.id.toString(), role: user.role?.roleName };
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof jwt.TokenExpiredError) {
|
||||||
|
return reject(
|
||||||
|
new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reject(
|
||||||
|
new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Express middleware — use as `auth()` in routes
|
||||||
|
*/
|
||||||
|
const auth =
|
||||||
|
() =>
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
verifyCallback(req, resolve, reject);
|
||||||
|
})
|
||||||
|
.then(() => next())
|
||||||
|
.catch((err) => next(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default auth;
|
||||||
103
src/common/middlewares/jwt/authForUser.ts
Normal file
103
src/common/middlewares/jwt/authForUser.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import httpStatus from 'http-status';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import ApiError from '../../utils/helper/ApiError';
|
||||||
|
import config from '../../../config/config';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
interface DecodedToken {
|
||||||
|
id: number;
|
||||||
|
role?: string;
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserPayload {
|
||||||
|
id: string;
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'express-serve-static-core' {
|
||||||
|
interface Request {
|
||||||
|
user?: UserPayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies JWT and validates Host user (role_xid = 3)
|
||||||
|
*/
|
||||||
|
const verifyCallback = async (
|
||||||
|
req: Request,
|
||||||
|
resolve: (value?: unknown) => void,
|
||||||
|
reject: (reason?: Error) => void
|
||||||
|
) => {
|
||||||
|
const token = req.header('x-auth-token') || req.cookies?.accessToken;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, config.jwt.secret) as DecodedToken;
|
||||||
|
|
||||||
|
if (!decoded?.id) {
|
||||||
|
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Fetch user from Prisma (Host user only)
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: decoded.id },
|
||||||
|
include: { role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'User not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Check if user is active
|
||||||
|
if (!user.isActive) {
|
||||||
|
return reject(
|
||||||
|
new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Check User role (role_xid = 1)
|
||||||
|
if (user.roleXid !== 1) {
|
||||||
|
return reject(
|
||||||
|
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach user to request
|
||||||
|
req.user = { id: user.id.toString(), role: user.role?.roleName };
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof jwt.TokenExpiredError) {
|
||||||
|
return reject(
|
||||||
|
new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reject(
|
||||||
|
new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Express middleware — use as `auth()` in routes
|
||||||
|
*/
|
||||||
|
const auth =
|
||||||
|
() =>
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
verifyCallback(req, resolve, reject);
|
||||||
|
})
|
||||||
|
.then(() => next())
|
||||||
|
.catch((err) => next(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default auth;
|
||||||
33
src/common/utils/helper/ApiError.ts
Normal file
33
src/common/utils/helper/ApiError.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
class ApiError<T = unknown> extends Error {
|
||||||
|
statusCode: number;
|
||||||
|
data: T | null;
|
||||||
|
message: string;
|
||||||
|
success: boolean;
|
||||||
|
errors: Array<Error>;
|
||||||
|
isOperational: boolean;
|
||||||
|
stack?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
statusCode: number,
|
||||||
|
message: string = 'Something went wrong',
|
||||||
|
errors: Array<Error> = [],
|
||||||
|
isOperational: boolean = true,
|
||||||
|
stack?: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.data = null;
|
||||||
|
this.message = message;
|
||||||
|
this.success = false;
|
||||||
|
this.errors = errors;
|
||||||
|
this.isOperational = isOperational;
|
||||||
|
|
||||||
|
if (stack) {
|
||||||
|
this.stack = stack;
|
||||||
|
} else {
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiError;
|
||||||
32
src/common/utils/pagination.util.ts
Normal file
32
src/common/utils/pagination.util.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { PaginationDto, PaginationMetaDto } from '../dto/pagination.dto';
|
||||||
|
|
||||||
|
export function createPaginationMeta(
|
||||||
|
paginationDto: PaginationDto,
|
||||||
|
total: number,
|
||||||
|
): PaginationMetaDto {
|
||||||
|
const { page = 1, limit = 10 } = paginationDto;
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
hasNext: page < totalPages,
|
||||||
|
hasPrev: page > 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function paginate<T>(
|
||||||
|
data: T[],
|
||||||
|
paginationDto: PaginationDto,
|
||||||
|
total: number,
|
||||||
|
) {
|
||||||
|
const meta = createPaginationMeta(paginationDto, total);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
185
src/config/config.ts
Normal file
185
src/config/config.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import dotenv from 'dotenv';
|
||||||
|
import path from 'path';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
dotenv.config({ path: path.join(__dirname, '../../.env') });
|
||||||
|
|
||||||
|
const envVarsSchema = yup
|
||||||
|
.object()
|
||||||
|
.shape({
|
||||||
|
NODE_ENV: yup
|
||||||
|
.string()
|
||||||
|
.oneOf(['production', 'development', 'test'])
|
||||||
|
.required(),
|
||||||
|
PORT: yup.number().default(3000),
|
||||||
|
BASEURL: yup.string().required('Base URL is required'),
|
||||||
|
// FRONTEND_URL: yup.string().required('Frontend URL is required'),
|
||||||
|
//JWT
|
||||||
|
JWT_SECRET: yup.string().required('JWT secret key is required'),
|
||||||
|
JWT_ACCESS_EXPIRATION_MINUTES: yup
|
||||||
|
.number()
|
||||||
|
.default(30)
|
||||||
|
.required('minutes after which access tokens expire'),
|
||||||
|
JWT_REFRESH_EXPIRATION_DAYS: yup
|
||||||
|
.number()
|
||||||
|
.default(30)
|
||||||
|
.required('days after which refresh tokens expire'),
|
||||||
|
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: yup
|
||||||
|
.number()
|
||||||
|
.default(10)
|
||||||
|
.required('minutes after which reset password token expires'),
|
||||||
|
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: yup
|
||||||
|
.number()
|
||||||
|
.default(10)
|
||||||
|
.required('minutes after which verify email token expires'),
|
||||||
|
//SMTP and BREVO
|
||||||
|
// BREVO_SMTP_HOST: yup
|
||||||
|
// .string()
|
||||||
|
// .nullable()
|
||||||
|
// .required('server that will send the emails'),
|
||||||
|
// BREVO_SMTP_PORT: yup
|
||||||
|
// .number()
|
||||||
|
// .nullable()
|
||||||
|
// .required('port to connect to the email server'),
|
||||||
|
// BREVO_SMTP_USER: yup
|
||||||
|
// .string()
|
||||||
|
// .nullable()
|
||||||
|
// .required('username for email server'),
|
||||||
|
// BREVO_SMTP_PASS: yup
|
||||||
|
// .string()
|
||||||
|
// .nullable()
|
||||||
|
// .required('password for email server'),
|
||||||
|
// BREVO_FROM_EMAIL: yup
|
||||||
|
// .string()
|
||||||
|
// .nullable()
|
||||||
|
// .required('the from field in the emails sent by the app'),
|
||||||
|
// BREVO_EMAIL_API_KEY: yup
|
||||||
|
// .string()
|
||||||
|
// .nullable()
|
||||||
|
// .required('the from field in the emails sent by the app api key'),
|
||||||
|
// BREVO_API_BASEURL: yup.string().required('Brevo base URL is required'),
|
||||||
|
// //one signal
|
||||||
|
// ONESIGNAL_APPID: yup.string().required('One signal app id is required'),
|
||||||
|
// ONESIGNAL_REST_APIKEY: yup
|
||||||
|
// .string()
|
||||||
|
// .required('One signal api key is required'),
|
||||||
|
//branch IO
|
||||||
|
// BRANCH_IO_KEY: yup.string().required('Branch IO key is required'),
|
||||||
|
|
||||||
|
// DataBase
|
||||||
|
DB_USERNAME: yup.string().required('DB Username is required'),
|
||||||
|
DB_PASSWORD: yup.string().required('DB Password is required'),
|
||||||
|
DB_DATABASE_NAME: yup.string().required('Database name is required'),
|
||||||
|
DB_HOSTNAME: yup
|
||||||
|
.string()
|
||||||
|
.default('127.0.0.1')
|
||||||
|
.required('DB Hostname is required'),
|
||||||
|
DB_PORT: yup.number().default(3306).required('DB Port is required'),
|
||||||
|
//OTP Bypass
|
||||||
|
BYPASS_OTP: yup.boolean().default(false).required('Bypass OTP is required'),
|
||||||
|
})
|
||||||
|
.noUnknown(true);
|
||||||
|
|
||||||
|
// Validate and prepare the configuration
|
||||||
|
function getConfig() {
|
||||||
|
try {
|
||||||
|
// Validate the environment variables
|
||||||
|
const envVars = envVarsSchema.validateSync(process.env, {
|
||||||
|
abortEarly: false, // Validate all fields before throwing errors
|
||||||
|
stripUnknown: true, // Remove fields not in the schema
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the validated configuration
|
||||||
|
return {
|
||||||
|
env: envVars.NODE_ENV,
|
||||||
|
port: envVars.PORT,
|
||||||
|
jwt: {
|
||||||
|
secret: envVars.JWT_SECRET,
|
||||||
|
accessExpirationMinutes: envVars.JWT_ACCESS_EXPIRATION_MINUTES,
|
||||||
|
refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS,
|
||||||
|
resetPasswordExpirationMinutes:
|
||||||
|
envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES,
|
||||||
|
verifyEmailExpirationMinutes:
|
||||||
|
envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES,
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
development: {
|
||||||
|
host: envVars.DB_HOSTNAME,
|
||||||
|
port: envVars.DB_PORT,
|
||||||
|
username: envVars.DB_USERNAME,
|
||||||
|
password: envVars.DB_PASSWORD,
|
||||||
|
database: envVars.DB_DATABASE_NAME,
|
||||||
|
logging: false,
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
host: envVars.DB_HOSTNAME,
|
||||||
|
port: envVars.DB_PORT,
|
||||||
|
username: envVars.DB_USERNAME,
|
||||||
|
password: envVars.DB_PASSWORD,
|
||||||
|
database: envVars.DB_DATABASE_NAME,
|
||||||
|
logging: false,
|
||||||
|
socketPath: '/var/run/mysqld/mysqld.sock',
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
host: envVars.DB_HOSTNAME,
|
||||||
|
port: envVars.DB_PORT,
|
||||||
|
username: envVars.DB_USERNAME,
|
||||||
|
password: envVars.DB_PASSWORD,
|
||||||
|
database: envVars.DB_DATABASE_NAME,
|
||||||
|
logging: false,
|
||||||
|
socketPath: '/var/run/mysqld/mysqld.sock',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
byPassOTP: envVars.BYPASS_OTP,
|
||||||
|
// BaseURL: envVars.BASEURL,
|
||||||
|
// FRONTEND_URL: envVars.FRONTEND_URL,
|
||||||
|
// email: {
|
||||||
|
// smtp: {
|
||||||
|
// host: envVars?.BREVO_SMTP_HOST,
|
||||||
|
// port: envVars?.BREVO_SMTP_PORT,
|
||||||
|
// secure: envVars?.BREVO_SMTP_PORT == 465, // true for 465, false for other ports
|
||||||
|
// auth: {
|
||||||
|
// user: envVars?.BREVO_SMTP_USER,
|
||||||
|
// pass: envVars?.BREVO_SMTP_PASS,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// from: envVars?.BREVO_FROM_EMAIL,
|
||||||
|
// api_key: envVars?.BREVO_EMAIL_API_KEY,
|
||||||
|
// BrevobaseURL: envVars?.BREVO_API_BASEURL,
|
||||||
|
// },
|
||||||
|
// oneSignal: {
|
||||||
|
// appID: envVars.ONESIGNAL_APPID,
|
||||||
|
// restApiKey: envVars.ONESIGNAL_REST_APIKEY,
|
||||||
|
// },
|
||||||
|
// branchIO: {
|
||||||
|
// branchIOKey: envVars.BRANCH_IO_KEY,
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof yup.ValidationError) {
|
||||||
|
console.error('Validation Errors:', error.errors.join(', '));
|
||||||
|
} else {
|
||||||
|
console.error('Unexpected error during configuration validation:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
'Server shut down due to incomplete environment variable configuration.'
|
||||||
|
);
|
||||||
|
process.exit(1); // Exit with error code 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created By : Angad Chauhan
|
||||||
|
* Created at : 31/1/25
|
||||||
|
* Use : For google login .env file global variable
|
||||||
|
*/
|
||||||
|
// export const googleConfig = {
|
||||||
|
// clientID: process.env.GOOGLE_CLIENT_ID!,
|
||||||
|
// clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||||
|
// callbackURL: process.env.GOOGLE_CALLBACK_URL!,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Validate and export configuration only if validation succeeds
|
||||||
|
const config = getConfig();
|
||||||
|
export default config;
|
||||||
82
src/main.ts
Normal file
82
src/main.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
|
import { writeFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
const configService = app.get(ConfigService);
|
||||||
|
|
||||||
|
// ✅ Security Middlewares
|
||||||
|
app.use(helmet());
|
||||||
|
app.enableCors({
|
||||||
|
origin: [
|
||||||
|
'http://localhost:3000', // Local Swagger UI
|
||||||
|
'http://localhost:3006', // Local Frontend
|
||||||
|
'https://editor.swagger.io', // Swagger Editor
|
||||||
|
'https://klc-admin.wdiprojects.com',
|
||||||
|
'https://admin-uat.klc.betadelivery.com',
|
||||||
|
],
|
||||||
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Global prefix
|
||||||
|
const globalPrefix = configService.get('API_PREFIX') || 'api/v1';
|
||||||
|
app.setGlobalPrefix(globalPrefix);
|
||||||
|
|
||||||
|
// ✅ Logging middleware
|
||||||
|
app.use((req, _res, next) => {
|
||||||
|
console.log(`📝 ${new Date().toISOString()} - ${req.method} ${req.url}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Global validation
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
transform: true,
|
||||||
|
transformOptions: {
|
||||||
|
enableImplicitConversion: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅ Swagger setup (only for local/dev)
|
||||||
|
if (configService.get('NODE_ENV') !== 'production') {
|
||||||
|
const swaggerConfig = new DocumentBuilder()
|
||||||
|
.setTitle('Minglar API')
|
||||||
|
.setDescription('NestJS Backend for Minglar with Lambda-ready endpoints')
|
||||||
|
.setVersion(configService.get('API_VERSION') || '1.0.0')
|
||||||
|
.addBearerAuth()
|
||||||
|
.addServer(`${process.env.SERVER_URL || 'http://localhost:3000'}/`, 'Local Server')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
||||||
|
SwaggerModule.setup('api/docs', app, document);
|
||||||
|
|
||||||
|
// Optionally write swagger.json for offline usage
|
||||||
|
const outputPath = join(process.cwd(), 'swagger.json');
|
||||||
|
writeFileSync(outputPath, JSON.stringify(document, null, 2), 'utf8');
|
||||||
|
console.log(`✅ Swagger JSON file generated at: ${outputPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Start app (only for local or non-Lambda)
|
||||||
|
const port = configService.get('PORT') || 3000;
|
||||||
|
await app.listen(port);
|
||||||
|
|
||||||
|
console.log(`🚀 Server running at http://localhost:${port}`);
|
||||||
|
if (configService.get('NODE_ENV') !== 'production') {
|
||||||
|
console.log(`📚 Swagger docs: http://localhost:${port}/api/docs`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only run bootstrap if not in AWS Lambda
|
||||||
|
if (!process.env.AWS_LAMBDA_FUNCTION_NAME) {
|
||||||
|
bootstrap();
|
||||||
|
}
|
||||||
0
src/modules/auth/auth.module.ts
Normal file
0
src/modules/auth/auth.module.ts
Normal file
51
src/modules/host/dto/host.dto.ts
Normal file
51
src/modules/host/dto/host.dto.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// src/modules/host/dto/host.dto.ts
|
||||||
|
import { IsInt, IsOptional, IsString, IsBoolean, IsEmail } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateHostDto {
|
||||||
|
@IsString()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
emailAddress: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
isdCode?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
mobileNumber?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
userPasscode?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
roleXid?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateHostDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
firstName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
lastName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
emailAddress?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
68
src/modules/host/handlers/host.handler.ts
Normal file
68
src/modules/host/handlers/host.handler.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// src/modules/host/handler/host.handler.ts
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from 'src/app.module';
|
||||||
|
import { HostService } from '../services/host.service';
|
||||||
|
import { APIGatewayProxyHandler } from 'aws-lambda';
|
||||||
|
import { CreateHostDto, UpdateHostDto } from '../dto/host.dto';
|
||||||
|
|
||||||
|
let app;
|
||||||
|
let hostService: HostService;
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
if (!app) {
|
||||||
|
const nestApp = await NestFactory.createApplicationContext(AppModule);
|
||||||
|
hostService = nestApp.get(HostService);
|
||||||
|
app = nestApp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handler: APIGatewayProxyHandler = async (event) => {
|
||||||
|
await bootstrap();
|
||||||
|
|
||||||
|
const method = event.httpMethod;
|
||||||
|
const path = event.path;
|
||||||
|
const body = event.body ? JSON.parse(event.body) : {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (method === 'POST' && path.endsWith('/host')) {
|
||||||
|
const host = await hostService.createHost(body as CreateHostDto);
|
||||||
|
return response(201, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'GET' && path.endsWith('/host')) {
|
||||||
|
const hosts = await hostService.getAllHosts();
|
||||||
|
return response(200, hosts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'GET' && path.match(/\/host\/\d+$/)) {
|
||||||
|
const id = Number(path.split('/').pop());
|
||||||
|
const host = await hostService.getHostById(id);
|
||||||
|
return response(200, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'PUT' && path.match(/\/host\/\d+$/)) {
|
||||||
|
const id = Number(path.split('/').pop());
|
||||||
|
const host = await hostService.updateHost(id, body as UpdateHostDto);
|
||||||
|
return response(200, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'DELETE' && path.match(/\/host\/\d+$/)) {
|
||||||
|
const id = Number(path.split('/').pop());
|
||||||
|
const host = await hostService.deleteHost(id);
|
||||||
|
return response(200, { message: 'Host deleted successfully', host });
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(404, { message: 'Not Found' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
return response(500, { message: error.message || 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function response(statusCode: number, body: any) {
|
||||||
|
return {
|
||||||
|
statusCode,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
};
|
||||||
|
}
|
||||||
34
src/modules/host/services/host.service.ts
Normal file
34
src/modules/host/services/host.service.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// src/modules/host/services/host.service.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../../../common/database/prisma.service';
|
||||||
|
import { CreateHostDto, UpdateHostDto } from '../dto/host.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HostService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async createHost(data: CreateHostDto) {
|
||||||
|
return this.prisma.user.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllHosts() {
|
||||||
|
return this.prisma.user.findMany({ where: { roleXid: 3 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHostById(id: number) {
|
||||||
|
const host = await this.prisma.user.findUnique({ where: { id } });
|
||||||
|
if (!host || host.roleXid !== 3) throw new Error('Host not found');
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateHost(id: number, data: UpdateHostDto) {
|
||||||
|
return this.prisma.user.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteHost(id: number) {
|
||||||
|
return this.prisma.user.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
27
swagger.json
Normal file
27
swagger.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"paths": {},
|
||||||
|
"info": {
|
||||||
|
"title": "Minglar API",
|
||||||
|
"description": "NestJS Backend for Minglar with Lambda-ready endpoints",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"contact": {}
|
||||||
|
},
|
||||||
|
"tags": [],
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:3000/",
|
||||||
|
"description": "Local Server"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"components": {
|
||||||
|
"securitySchemes": {
|
||||||
|
"bearer": {
|
||||||
|
"scheme": "bearer",
|
||||||
|
"bearerFormat": "JWT",
|
||||||
|
"type": "http"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2020",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@/common/*": ["src/common/*"],
|
||||||
|
"@/modules/*": ["src/modules/*"],
|
||||||
|
"@/config/*": ["src/config/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user