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
|
||||
20
env.dev
Normal file
20
env.dev
Normal file
@@ -0,0 +1,20 @@
|
||||
# Development Environment
|
||||
NODE_ENV=development
|
||||
DATABASE_URL="postgresql://username:password@localhost:5432/nestjs_user_crud_dev?schema=public"
|
||||
JWT_SECRET="dev-jwt-secret-key-make-it-long-and-random"
|
||||
JWT_EXPIRES_IN="7d"
|
||||
API_PREFIX="api/v1"
|
||||
API_VERSION="1.0.0"
|
||||
THROTTLE_TTL=60
|
||||
THROTTLE_LIMIT=10
|
||||
CORS_ORIGIN="http://localhost:3000"
|
||||
|
||||
# AWS Configuration (for local development)
|
||||
AWS_REGION=us-east-1
|
||||
AWS_ACCESS_KEY_ID=your-access-key
|
||||
AWS_SECRET_ACCESS_KEY=your-secret-key
|
||||
|
||||
# VPC Configuration (if using RDS in VPC)
|
||||
VPC_SECURITY_GROUP_ID=sg-xxxxxxxxx
|
||||
VPC_SUBNET_ID_1=subnet-xxxxxxxxx
|
||||
VPC_SUBNET_ID_2=subnet-yyyyyyyyy
|
||||
22
env.example
Normal file
22
env.example
Normal file
@@ -0,0 +1,22 @@
|
||||
# Database
|
||||
DATABASE_URL="postgresql://username:password@localhost:5432/nestjs_user_crud?schema=public"
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET="your-super-secret-jwt-key-here"
|
||||
JWT_EXPIRES_IN="7d"
|
||||
|
||||
# Application
|
||||
PORT=3000
|
||||
NODE_ENV="development"
|
||||
|
||||
# API Configuration
|
||||
API_PREFIX="api/v1"
|
||||
API_VERSION="1.0.0"
|
||||
|
||||
# Rate Limiting
|
||||
THROTTLE_TTL=60
|
||||
THROTTLE_LIMIT=10
|
||||
|
||||
# CORS
|
||||
CORS_ORIGIN="http://localhost:3000"
|
||||
|
||||
20
env.prod
Normal file
20
env.prod
Normal file
@@ -0,0 +1,20 @@
|
||||
# Production Environment
|
||||
NODE_ENV=production
|
||||
DATABASE_URL="postgresql://username:password@prod-db-host:5432/nestjs_user_crud_prod?schema=public"
|
||||
JWT_SECRET="prod-jwt-secret-key-make-it-long-and-random"
|
||||
JWT_EXPIRES_IN="7d"
|
||||
API_PREFIX="api/v1"
|
||||
API_VERSION="1.0.0"
|
||||
THROTTLE_TTL=60
|
||||
THROTTLE_LIMIT=10
|
||||
CORS_ORIGIN="https://yourdomain.com"
|
||||
|
||||
# AWS Configuration
|
||||
AWS_REGION=us-east-1
|
||||
AWS_ACCESS_KEY_ID=your-prod-access-key
|
||||
AWS_SECRET_ACCESS_KEY=your-prod-secret-key
|
||||
|
||||
# VPC Configuration
|
||||
VPC_SECURITY_GROUP_ID=sg-prod-xxxxxxxxx
|
||||
VPC_SUBNET_ID_1=subnet-prod-xxxxxxxxx
|
||||
VPC_SUBNET_ID_2=subnet-prod-yyyyyyyyy
|
||||
20
env.staging
Normal file
20
env.staging
Normal file
@@ -0,0 +1,20 @@
|
||||
# Staging Environment
|
||||
NODE_ENV=staging
|
||||
DATABASE_URL="postgresql://username:password@staging-db-host:5432/nestjs_user_crud_staging?schema=public"
|
||||
JWT_SECRET="staging-jwt-secret-key-make-it-long-and-random"
|
||||
JWT_EXPIRES_IN="7d"
|
||||
API_PREFIX="api/v1"
|
||||
API_VERSION="1.0.0"
|
||||
THROTTLE_TTL=60
|
||||
THROTTLE_LIMIT=10
|
||||
CORS_ORIGIN="https://staging.yourdomain.com"
|
||||
|
||||
# AWS Configuration
|
||||
AWS_REGION=us-east-1
|
||||
AWS_ACCESS_KEY_ID=your-staging-access-key
|
||||
AWS_SECRET_ACCESS_KEY=your-staging-secret-key
|
||||
|
||||
# VPC Configuration
|
||||
VPC_SECURITY_GROUP_ID=sg-staging-xxxxxxxxx
|
||||
VPC_SUBNET_ID_1=subnet-staging-xxxxxxxxx
|
||||
VPC_SUBNET_ID_2=subnet-staging-yyyyyyyyy
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
9241
package-lock.json
generated
Normal file
9241
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
92
package.json
Normal file
92
package.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"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/core": "^10.3.0",
|
||||
"@nestjs/platform-express": "^10.3.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/swagger": "^7.1.17",
|
||||
"@nestjs/throttler": "^5.1.1",
|
||||
"@prisma/client": "^5.8.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"helmet": "^7.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",
|
||||
"swagger-ui-express": "^5.0.0"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
154
prisma/schema.prisma
Normal file
154
prisma/schema.prisma
Normal file
@@ -0,0 +1,154 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["multiSchema"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
schemas = ["cnt", "usr"]
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
username String @unique
|
||||
password String
|
||||
firstName String?
|
||||
lastName String?
|
||||
isActive Boolean @default(true)
|
||||
role UserRole @default(USER)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
|
||||
@@map("users")
|
||||
@@schema("usr")
|
||||
}
|
||||
|
||||
enum UserRole {
|
||||
USER
|
||||
ADMIN
|
||||
HR
|
||||
}
|
||||
|
||||
model Blog {
|
||||
id Int @id @default(autoincrement())
|
||||
title String @map("title")
|
||||
urlSlug String? @unique @map("url_slug") // optional canonical URL
|
||||
content String @map("content") @db.Text
|
||||
bannerImage String? @map("banner_image") // URL to banner image
|
||||
category String? @map("category")
|
||||
tags String[] @default([]) @map("tags")
|
||||
metaTitle String? @map("meta_title")
|
||||
metaDesc String? @map("meta_desc") @db.Text
|
||||
publishedAt DateTime? @map("published_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
@@map("blogs")
|
||||
@@schema("cnt")
|
||||
}
|
||||
|
||||
model FAQ {
|
||||
id Int @id @default(autoincrement())
|
||||
question String @map("question")
|
||||
category String? @map("category")
|
||||
answer String @map("answer")
|
||||
tags String[] @default([]) @map("tags")
|
||||
globalTag String[] @default([]) @map("global_tag")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
@@map("faqs")
|
||||
@@schema("cnt")
|
||||
}
|
||||
|
||||
model Webcast {
|
||||
id Int @id @default(autoincrement())
|
||||
title String @map("title")
|
||||
description String @map("description")
|
||||
fileUrl String @map("file_url")
|
||||
tags String[] @default([]) @map("tags")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
@@map("webcasts")
|
||||
@@schema("cnt")
|
||||
}
|
||||
|
||||
model TrainingMaterials {
|
||||
id Int @id @default(autoincrement())
|
||||
title String @map("title")
|
||||
description String @map("description")
|
||||
fileUrl String @map("file_url")
|
||||
tags String[] @default([]) @map("tags")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
@@map("training_materials")
|
||||
@@schema("cnt")
|
||||
}
|
||||
|
||||
model ReadingMaterials {
|
||||
id Int @id @default(autoincrement())
|
||||
title String @map("title")
|
||||
description String @map("description")
|
||||
fileUrl String @map("file_url")
|
||||
tags String[] @default([]) @map("tags")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
@@map("reading_materials")
|
||||
@@schema("cnt")
|
||||
}
|
||||
|
||||
model Podcasts {
|
||||
id Int @id @default(autoincrement())
|
||||
title String @map("title")
|
||||
description String @map("description")
|
||||
fileUrl String @map("file_url")
|
||||
tags String[] @default([]) @map("tags")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
@@map("podcasts")
|
||||
@@schema("cnt")
|
||||
}
|
||||
|
||||
model CaseStudies {
|
||||
id Int @id @default(autoincrement())
|
||||
title String @map("title")
|
||||
description String @map("description")
|
||||
fileUrl String @map("file_url")
|
||||
tags String[] @default([]) @map("tags")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
@@map("case_studies")
|
||||
@@schema("cnt")
|
||||
}
|
||||
|
||||
model KlcArchive {
|
||||
id Int @id @default(autoincrement())
|
||||
title String @map("title")
|
||||
description String @map("description")
|
||||
fileUrl String @map("file_url")
|
||||
tags String[] @default([]) @map("tags")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("delecase_studies
|
||||
@@map("klc_archive")
|
||||
@@schema("cnt")
|
||||
}
|
||||
68
prisma/seed.ts
Normal file
68
prisma/seed.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
// Create admin user
|
||||
const adminPassword = await bcrypt.hash('admin123', 10);
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: 'admin@example.com' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'admin@example.com',
|
||||
username: 'admin',
|
||||
password: adminPassword,
|
||||
firstName: 'Admin',
|
||||
lastName: 'User',
|
||||
},
|
||||
});
|
||||
|
||||
// Create regular user
|
||||
const userPassword = await bcrypt.hash('user123', 10);
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: 'user@example.com' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'user@example.com',
|
||||
username: 'user',
|
||||
password: userPassword,
|
||||
firstName: 'Regular',
|
||||
lastName: 'User',
|
||||
},
|
||||
});
|
||||
|
||||
// Create sample blogs
|
||||
await prisma.blog.createMany({
|
||||
data: [
|
||||
{
|
||||
title: 'Welcome to KLC Backend',
|
||||
content: 'This is a sample blog post created by the admin user.',
|
||||
category: 'General',
|
||||
tags: ['welcome', 'introduction'],
|
||||
publishedAt: new Date(),
|
||||
},
|
||||
{
|
||||
title: 'Getting Started with Prisma',
|
||||
content: 'Learn how to use Prisma ORM with NestJS.',
|
||||
category: 'Technical',
|
||||
tags: ['prisma', 'database', 'tutorial'],
|
||||
publishedAt: new Date(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log('Seed data created successfully!');
|
||||
console.log('Admin user:', { email: admin.email, username: admin.username });
|
||||
console.log('Regular user:', { email: user.email, username: user.username });
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
24
src/app.controller.ts
Normal file
24
src/app.controller.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@ApiTags('App')
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get application health status' })
|
||||
@ApiResponse({ status: 200, description: 'Application is running' })
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
@Get('health')
|
||||
@ApiOperation({ summary: 'Health check endpoint' })
|
||||
@ApiResponse({ status: 200, description: 'Health check successful' })
|
||||
getHealth() {
|
||||
return this.appService.getHealth();
|
||||
}
|
||||
}
|
||||
|
||||
37
src/app.module.ts
Normal file
37
src/app.module.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { PrismaModule } from './common/database/prisma.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { BlogsModule } from './modules/blog/blogs.module';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// Configuration
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
|
||||
// Rate limiting
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 60000, // 1 minute
|
||||
limit: 10, // 10 requests per minute
|
||||
},
|
||||
]),
|
||||
|
||||
// Database
|
||||
PrismaModule,
|
||||
|
||||
// Feature modules
|
||||
AuthModule,
|
||||
BlogsModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
18
src/app.service.ts
Normal file
18
src/app.service.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Welcome to NestJS User CRUD API!';
|
||||
}
|
||||
|
||||
getHealth() {
|
||||
return {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
66
src/main.ts
Normal file
66
src/main.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
const configService = app.get(ConfigService);
|
||||
|
||||
// Security
|
||||
app.use(helmet());
|
||||
app.enableCors({
|
||||
origin: configService.get('CORS_ORIGIN', 'http://localhost:3000'),
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// Global prefix
|
||||
app.setGlobalPrefix(configService.get('API_PREFIX', 'api/v1'));
|
||||
|
||||
// Request logging middleware
|
||||
app.use((req, res, next) => {
|
||||
console.log(`📝 ${new Date().toISOString()} - ${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
// Global validation pipe
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Swagger documentation (only in development)
|
||||
if (configService.get('NODE_ENV') !== 'production') {
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('NestJS Serverless User CRUD API')
|
||||
.setDescription('Professional NestJS serverless application with user CRUD and authentication')
|
||||
.setVersion(configService.get('API_VERSION', '1.0.0'))
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('api/docs', app, document);
|
||||
}
|
||||
|
||||
const port = configService.get('PORT', 3000);
|
||||
await app.listen(port);
|
||||
|
||||
console.log(`🚀 Application is running on: http://localhost:${port}`);
|
||||
if (configService.get('NODE_ENV') !== 'production') {
|
||||
console.log(`📚 Swagger documentation: http://localhost:${port}/api/docs`);
|
||||
}
|
||||
}
|
||||
|
||||
// Only bootstrap if not running in Lambda
|
||||
if (process.env.AWS_LAMBDA_FUNCTION_NAME === undefined) {
|
||||
bootstrap();
|
||||
}
|
||||
|
||||
28
src/modules/auth/auth.module.ts
Normal file
28
src/modules/auth/auth.module.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuthController } from './controllers/auth.controller';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { LocalStrategy } from './strategies/local.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('JWT_SECRET'),
|
||||
signOptions: {
|
||||
expiresIn: configService.get<string>('JWT_EXPIRES_IN', '7d'),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy, LocalStrategy],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
42
src/modules/auth/controllers/auth.controller.ts
Normal file
42
src/modules/auth/controllers/auth.controller.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { LoginDto } from '../dto/login.dto';
|
||||
import { RegisterDto } from '../dto/register.dto';
|
||||
import { AuthResponseDto } from '../dto/auth-response.dto';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||
|
||||
@ApiTags('Authentication')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('register')
|
||||
@ApiOperation({ summary: 'Register a new user' })
|
||||
@ApiResponse({ status: 201, description: 'User registered successfully', type: AuthResponseDto })
|
||||
@ApiResponse({ status: 400, description: 'Bad request' })
|
||||
@ApiResponse({ status: 409, description: 'User already exists' })
|
||||
async register(@Body() registerDto: RegisterDto): Promise<AuthResponseDto> {
|
||||
return this.authService.register(registerDto);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Login user' })
|
||||
@ApiResponse({ status: 200, description: 'Login successful', type: AuthResponseDto })
|
||||
@ApiResponse({ status: 401, description: 'Invalid credentials' })
|
||||
async login(@Body() loginDto: LoginDto): Promise<AuthResponseDto> {
|
||||
return this.authService.login(loginDto);
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Refresh access token' })
|
||||
@ApiResponse({ status: 200, description: 'Token refreshed successfully', type: AuthResponseDto })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async refresh(@Body() body: { refreshToken: string }): Promise<AuthResponseDto> {
|
||||
return this.authService.refreshToken(body.refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
23
src/modules/auth/dto/auth-response.dto.ts
Normal file
23
src/modules/auth/dto/auth-response.dto.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { User } from '@prisma/client';
|
||||
|
||||
export class AuthResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'User information (without password)',
|
||||
type: 'object',
|
||||
})
|
||||
user: Omit<User, 'password'>;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'JWT access token',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
})
|
||||
accessToken: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'JWT refresh token',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
})
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
21
src/modules/auth/dto/login.dto.ts
Normal file
21
src/modules/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { IsEmail, IsString, MinLength } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({
|
||||
description: 'User email address',
|
||||
example: 'user@example.com',
|
||||
})
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'User password',
|
||||
example: 'password123',
|
||||
minLength: 6,
|
||||
})
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
password: string;
|
||||
}
|
||||
|
||||
49
src/modules/auth/dto/register.dto.ts
Normal file
49
src/modules/auth/dto/register.dto.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { IsEmail, IsString, MinLength, IsOptional, Matches } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class RegisterDto {
|
||||
@ApiProperty({
|
||||
description: 'User email address',
|
||||
example: 'user@example.com',
|
||||
})
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Username (unique)',
|
||||
example: 'johndoe',
|
||||
minLength: 3,
|
||||
})
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
@Matches(/^[a-zA-Z0-9_]+$/, {
|
||||
message: 'Username can only contain letters, numbers, and underscores',
|
||||
})
|
||||
username: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'User password',
|
||||
example: 'password123',
|
||||
minLength: 6,
|
||||
})
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
password: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'First name',
|
||||
example: 'John',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
firstName?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Last name',
|
||||
example: 'Doe',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastName?: string;
|
||||
}
|
||||
|
||||
6
src/modules/auth/guards/jwt-auth.guard.ts
Normal file
6
src/modules/auth/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||
|
||||
6
src/modules/auth/guards/local-auth.guard.ts
Normal file
6
src/modules/auth/guards/local-auth.guard.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class LocalAuthGuard extends AuthGuard('local') {}
|
||||
|
||||
150
src/modules/auth/services/auth.service.ts
Normal file
150
src/modules/auth/services/auth.service.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { PrismaService } from '../../../common/database/prisma.service';
|
||||
import { LoginDto } from '../dto/login.dto';
|
||||
import { RegisterDto } from '../dto/register.dto';
|
||||
import { AuthResponseDto } from '../dto/auth-response.dto';
|
||||
import { JwtPayload } from '../../../common/interfaces/auth.interface';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async register(registerDto: RegisterDto): Promise<AuthResponseDto> {
|
||||
const { email, username, password, firstName, lastName } = registerDto;
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ email }, { username }],
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
throw new ConflictException('User with this email or username already exists');
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Create user
|
||||
const user = await this.prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
username,
|
||||
password: hashedPassword,
|
||||
firstName,
|
||||
lastName,
|
||||
},
|
||||
});
|
||||
|
||||
// Generate tokens
|
||||
const tokens = await this.generateTokens(user);
|
||||
|
||||
return {
|
||||
user: this.excludePassword(user),
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
|
||||
async login(loginDto: LoginDto): Promise<AuthResponseDto> {
|
||||
const { email, password } = loginDto;
|
||||
|
||||
// Find user
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
// Check if user is active
|
||||
if (!user.isActive) {
|
||||
throw new UnauthorizedException('Account is deactivated');
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const tokens = await this.generateTokens(user);
|
||||
|
||||
return {
|
||||
user: this.excludePassword(user),
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthResponseDto> {
|
||||
try {
|
||||
const payload = this.jwtService.verify(refreshToken, {
|
||||
secret: this.configService.get<string>('JWT_SECRET'),
|
||||
});
|
||||
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: payload.sub },
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
const tokens = await this.generateTokens(user);
|
||||
|
||||
return {
|
||||
user: this.excludePassword(user),
|
||||
...tokens,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
}
|
||||
|
||||
async validateUser(email: string, password: string): Promise<any> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (user && (await bcrypt.compare(password, user.password))) {
|
||||
return this.excludePassword(user);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async generateTokens(user: any) {
|
||||
const payload: JwtPayload = {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
role: 'USER', // Default role since we removed the role field
|
||||
};
|
||||
|
||||
const [accessToken, refreshToken] = await Promise.all([
|
||||
this.jwtService.signAsync(payload),
|
||||
this.jwtService.signAsync(payload, {
|
||||
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRES_IN', '30d'),
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
private excludePassword(user: any) {
|
||||
const { password, ...result } = user;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
33
src/modules/auth/strategies/jwt.strategy.ts
Normal file
33
src/modules/auth/strategies/jwt.strategy.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PrismaService } from '../../../common/database/prisma.service';
|
||||
import { JwtPayload } from '../../../common/interfaces/auth.interface';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prisma: PrismaService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get<string>('JWT_SECRET'),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: payload.sub },
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
22
src/modules/auth/strategies/local.strategy.ts
Normal file
22
src/modules/auth/strategies/local.strategy.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-local';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private readonly authService: AuthService) {
|
||||
super({
|
||||
usernameField: 'email',
|
||||
});
|
||||
}
|
||||
|
||||
async validate(email: string, password: string): Promise<any> {
|
||||
const user = await this.authService.validateUser(email, password);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
13
src/modules/blog/blogs.module.ts
Normal file
13
src/modules/blog/blogs.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BlogsController } from './controllers/blogs.controller';
|
||||
import { BlogsService } from './services/blogs.service';
|
||||
import { PrismaModule } from '../../common/database/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [BlogsController],
|
||||
providers: [BlogsService],
|
||||
exports: [BlogsService],
|
||||
})
|
||||
export class BlogsModule {}
|
||||
|
||||
112
src/modules/blog/controllers/blogs.controller.ts
Normal file
112
src/modules/blog/controllers/blogs.controller.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { BlogsService } from '../services/blogs.service';
|
||||
import { CreateBlogDto } from '../dto/create-blog.dto';
|
||||
import { UpdateBlogDto } from '../dto/update-blog.dto';
|
||||
import { BlogResponseDto } from '../dto/blog-response.dto';
|
||||
import { PaginationDto } from '../../../common/dto/pagination.dto';
|
||||
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../../../common/guards/roles.guard';
|
||||
import { Roles } from '../../../common/decorators/roles.decorator';
|
||||
|
||||
@ApiTags('blogs')
|
||||
@Controller('blogs')
|
||||
export class BlogsController {
|
||||
constructor(private readonly blogsService: BlogsService) {}
|
||||
|
||||
@Post()
|
||||
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||
// @Roles('ADMIN', 'HR')
|
||||
// @ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Create a new blog post' })
|
||||
@ApiResponse({ status: 201, description: 'Blog created successfully', type: BlogResponseDto })
|
||||
// @ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
// @ApiResponse({ status: 403, description: 'Forbidden' })
|
||||
async create(@Body() createBlogDto: CreateBlogDto): Promise<BlogResponseDto> {
|
||||
return this.blogsService.create(createBlogDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all blog posts with pagination' })
|
||||
@ApiResponse({ status: 200, description: 'Blogs retrieved successfully' })
|
||||
async findAll(@Query() paginationDto: PaginationDto) {
|
||||
return this.blogsService.findAll(paginationDto);
|
||||
}
|
||||
|
||||
@Get('category/:category')
|
||||
@ApiOperation({ summary: 'Get blog posts by category' })
|
||||
@ApiResponse({ status: 200, description: 'Blogs retrieved successfully' })
|
||||
async findByCategory(
|
||||
@Param('category') category: string,
|
||||
@Query() paginationDto: PaginationDto,
|
||||
) {
|
||||
return this.blogsService.findByCategory(category, paginationDto);
|
||||
}
|
||||
|
||||
@Get('tag/:tag')
|
||||
@ApiOperation({ summary: 'Get blog posts by tag' })
|
||||
@ApiResponse({ status: 200, description: 'Blogs retrieved successfully' })
|
||||
async findByTag(
|
||||
@Param('tag') tag: string,
|
||||
@Query() paginationDto: PaginationDto,
|
||||
) {
|
||||
return this.blogsService.findByTag(tag, paginationDto);
|
||||
}
|
||||
|
||||
@Get('slug/:urlSlug')
|
||||
@ApiOperation({ summary: 'Get blog post by URL slug' })
|
||||
@ApiResponse({ status: 200, description: 'Blog retrieved successfully', type: BlogResponseDto })
|
||||
@ApiResponse({ status: 404, description: 'Blog not found' })
|
||||
async findBySlug(@Param('urlSlug') urlSlug: string): Promise<BlogResponseDto> {
|
||||
return this.blogsService.findBySlug(urlSlug);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get blog post by ID' })
|
||||
@ApiResponse({ status: 200, description: 'Blog retrieved successfully', type: BlogResponseDto })
|
||||
@ApiResponse({ status: 404, description: 'Blog not found' })
|
||||
async findOne(@Param('id', ParseIntPipe) id: number): Promise<BlogResponseDto> {
|
||||
return this.blogsService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('ADMIN', 'HR')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Update blog post' })
|
||||
@ApiResponse({ status: 200, description: 'Blog updated successfully', type: BlogResponseDto })
|
||||
@ApiResponse({ status: 404, description: 'Blog not found' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
@ApiResponse({ status: 403, description: 'Forbidden' })
|
||||
async update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateBlogDto: UpdateBlogDto,
|
||||
): Promise<BlogResponseDto> {
|
||||
return this.blogsService.update(id, updateBlogDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('ADMIN', 'HR')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Delete blog post (soft delete)' })
|
||||
@ApiResponse({ status: 200, description: 'Blog deleted successfully' })
|
||||
@ApiResponse({ status: 404, description: 'Blog not found' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
@ApiResponse({ status: 403, description: 'Forbidden' })
|
||||
async remove(@Param('id', ParseIntPipe) id: number): Promise<{ message: string }> {
|
||||
await this.blogsService.remove(id);
|
||||
return { message: 'Blog deleted successfully' };
|
||||
}
|
||||
}
|
||||
33
src/modules/blog/dto/blog-response.dto.ts
Normal file
33
src/modules/blog/dto/blog-response.dto.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class BlogResponseDto {
|
||||
@ApiProperty({ description: 'Blog ID' })
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: 'Blog title' })
|
||||
title: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'URL slug for the blog' })
|
||||
urlSlug?: string;
|
||||
|
||||
@ApiProperty({ description: 'Blog content' })
|
||||
content: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Banner image URL' })
|
||||
bannerImage?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Blog category' })
|
||||
category?: string;
|
||||
|
||||
@ApiProperty({ description: 'Blog tags', type: [String] })
|
||||
tags: string[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Meta title for SEO' })
|
||||
metaTitle?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Meta description for SEO' })
|
||||
metaDesc?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Publication date' })
|
||||
publishedAt?: Date;
|
||||
}
|
||||
48
src/modules/blog/dto/create-blog.dto.ts
Normal file
48
src/modules/blog/dto/create-blog.dto.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { IsString, IsOptional, IsArray, IsDateString } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateBlogDto {
|
||||
@ApiProperty({ description: 'Blog title' })
|
||||
@IsString()
|
||||
title: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'URL slug for the blog' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
urlSlug?: string;
|
||||
|
||||
@ApiProperty({ description: 'Blog content' })
|
||||
@IsString()
|
||||
content: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Banner image URL' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
bannerImage?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Blog category' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
category?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Blog tags', type: [String] })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
tags?: string[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Meta title for SEO' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
metaTitle?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Meta description for SEO' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
metaDesc?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Publication date' })
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
publishedAt?: Date;
|
||||
}
|
||||
4
src/modules/blog/dto/update-blog.dto.ts
Normal file
4
src/modules/blog/dto/update-blog.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateBlogDto } from './create-blog.dto';
|
||||
|
||||
export class UpdateBlogDto extends PartialType(CreateBlogDto) {}
|
||||
200
src/modules/blog/services/blogs.service.ts
Normal file
200
src/modules/blog/services/blogs.service.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../common/database/prisma.service';
|
||||
import { CreateBlogDto } from '../dto/create-blog.dto';
|
||||
import { UpdateBlogDto } from '../dto/update-blog.dto';
|
||||
import { BlogResponseDto } from '../dto/blog-response.dto';
|
||||
import { PaginationDto } from '../../../common/dto/pagination.dto';
|
||||
import { paginate } from '../../../common/utils/pagination.util';
|
||||
|
||||
@Injectable()
|
||||
export class BlogsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async create(createBlogDto: CreateBlogDto): Promise<BlogResponseDto> {
|
||||
const blog = await this.prisma.blog.create({
|
||||
data: {
|
||||
...createBlogDto,
|
||||
tags: createBlogDto.tags || [],
|
||||
},
|
||||
});
|
||||
|
||||
return this.mapToResponseDto(blog);
|
||||
}
|
||||
|
||||
async findAll(paginationDto: PaginationDto) {
|
||||
const { page, limit } = paginationDto;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [blogs, total] = await Promise.all([
|
||||
this.prisma.blog.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
this.prisma.blog.count({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const mappedBlogs = blogs.map(blog => this.mapToResponseDto(blog));
|
||||
|
||||
return paginate(mappedBlogs, { page, limit }, total);
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<BlogResponseDto> {
|
||||
const blog = await this.prisma.blog.findFirst({
|
||||
where: {
|
||||
id,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!blog) {
|
||||
throw new NotFoundException(`Blog with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return this.mapToResponseDto(blog);
|
||||
}
|
||||
|
||||
async findBySlug(urlSlug: string): Promise<BlogResponseDto> {
|
||||
const blog = await this.prisma.blog.findFirst({
|
||||
where: {
|
||||
urlSlug,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!blog) {
|
||||
throw new NotFoundException(`Blog with slug ${urlSlug} not found`);
|
||||
}
|
||||
|
||||
return this.mapToResponseDto(blog);
|
||||
}
|
||||
|
||||
async update(id: number, updateBlogDto: UpdateBlogDto): Promise<BlogResponseDto> {
|
||||
const existingBlog = await this.prisma.blog.findFirst({
|
||||
where: {
|
||||
id,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingBlog) {
|
||||
throw new NotFoundException(`Blog with ID ${id} not found`);
|
||||
}
|
||||
|
||||
const blog = await this.prisma.blog.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...updateBlogDto,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return this.mapToResponseDto(blog);
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<void> {
|
||||
const existingBlog = await this.prisma.blog.findFirst({
|
||||
where: {
|
||||
id,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingBlog) {
|
||||
throw new NotFoundException(`Blog with ID ${id} not found`);
|
||||
}
|
||||
|
||||
await this.prisma.blog.update({
|
||||
where: { id },
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findByCategory(category: string, paginationDto: PaginationDto) {
|
||||
const { page, limit } = paginationDto;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [blogs, total] = await Promise.all([
|
||||
this.prisma.blog.findMany({
|
||||
where: {
|
||||
category,
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
this.prisma.blog.count({
|
||||
where: {
|
||||
category,
|
||||
deletedAt: null,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const mappedBlogs = blogs.map(blog => this.mapToResponseDto(blog));
|
||||
|
||||
return paginate(mappedBlogs, { page, limit }, total);
|
||||
}
|
||||
|
||||
async findByTag(tag: string, paginationDto: PaginationDto) {
|
||||
const { page, limit } = paginationDto;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [blogs, total] = await Promise.all([
|
||||
this.prisma.blog.findMany({
|
||||
where: {
|
||||
tags: {
|
||||
has: tag,
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
this.prisma.blog.count({
|
||||
where: {
|
||||
tags: {
|
||||
has: tag,
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const mappedBlogs = blogs.map(blog => this.mapToResponseDto(blog));
|
||||
|
||||
return paginate(mappedBlogs, { page, limit }, total);
|
||||
}
|
||||
|
||||
private mapToResponseDto(blog: any): BlogResponseDto {
|
||||
return {
|
||||
id: blog.id,
|
||||
title: blog.title,
|
||||
urlSlug: blog.urlSlug,
|
||||
content: blog.content,
|
||||
bannerImage: blog.bannerImage,
|
||||
category: blog.category,
|
||||
tags: blog.tags,
|
||||
metaTitle: blog.metaTitle,
|
||||
metaDesc: blog.metaDesc,
|
||||
publishedAt: blog.publishedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
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