17 Commits
Release ... Dev

Author SHA1 Message Date
test
e38a88f4c7 chore(ci): add no-op var to trigger pipeline
All checks were successful
DevSecOps-Multibranch/pipeline/head This commit looks good
2025-11-30 19:36:34 +05:30
test
eecc3ad9e2 Fix: Proxy to Docker bridge gateway (172.17.0.1) so frontend can reach backend 2025-11-30 19:25:16 +05:30
test
e07af65f67 Fix: Use 127.0.0.1 for nginx proxy to fix 502 error
All checks were successful
DevSecOps-Multibranch/pipeline/head This commit looks good
2025-11-30 19:15:58 +05:30
test
101cbf86d9 Fix: Change nginx backend proxy from hostname to localhost
All checks were successful
DevSecOps-Multibranch/pipeline/head This commit looks good
2025-11-30 19:07:52 +05:30
test
a4357c95cd Fix: Remove hardcoded secrets, fix port mappings, and update frontend proxy
All checks were successful
DevSecOps-Multibranch/pipeline/head This commit looks good
2025-11-30 19:00:41 +05:30
test
c2abaaa2c1 Update Jenkinsfile: separate backend and frontend deployment with dynamic ports
All checks were successful
DevSecOps-Multibranch/pipeline/head This commit looks good
2025-11-30 18:54:21 +05:30
627d04f6d4 fix: Jenkinsfile syntax and structure for dual backend/frontend deployment 2025-11-30 18:49:08 +05:30
8eab022a91 feat: deploy both backend (3001) and frontend (3002) on shared Docker network for Dev; backend listens on 3001; robust for gitea server
Some checks failed
DevSecOps-Multibranch/pipeline/head There was a failure building this commit
2025-11-30 18:45:15 +05:30
8292dcb756 fix: backend Dockerfile copies all files, server.js uses port 3000 for container consistency
All checks were successful
DevSecOps-Multibranch/pipeline/head This commit looks good
2025-11-30 18:35:11 +05:30
22e842bf87 updated dockerFile
All checks were successful
DevSecOps-Multibranch/pipeline/head This commit looks good
2025-11-30 12:52:29 +00:00
cd3b5d242d updated host port
All checks were successful
DevSecOps-Multibranch/pipeline/head This commit looks good
2025-11-30 12:43:39 +00:00
ecb4417f2f Updated jenkinsFile
All checks were successful
DevSecOps-Multibranch/pipeline/head This commit looks good
2025-11-30 12:30:43 +00:00
eb85e581f3 Dev: Update Jenkinsfile — use core repo in registry, secure login/logout, cleanup in post
All checks were successful
DevSecOps-Multibranch/pipeline/head This commit looks good
2025-11-30 16:01:58 +05:30
266b9b59c2 Update Push to Registry stage with secure credentials handling for DigitalOcean
Some checks failed
DevSecOps-Multibranch/pipeline/head There was a failure building this commit
2025-11-30 15:47:18 +05:30
e934074647 Update Jenkinsfile with optimized pipeline using shared repository and distinct image tags
Some checks failed
DevSecOps-Multibranch/pipeline/head There was a failure building this commit
2025-11-30 15:37:41 +05:30
74f4364fd4 Push main branch code to Dev branch
Some checks failed
DevSecOps-Multibranch/pipeline/head There was a failure building this commit
2025-11-30 15:28:57 +05:30
ee03ef39ca Clear all files from Dev branch 2025-11-30 15:28:37 +05:30
22 changed files with 22161 additions and 52 deletions

44
.gitignore vendored
View File

@@ -1,3 +1,20 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Production builds
/build
/dist
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE # IDE
.vscode/ .vscode/
.idea/ .idea/
@@ -8,15 +25,22 @@
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Dependencies # Logs
node_modules/ logs/
package-lock.json *.log
yarn.lock
# Build # Runtime data
dist/ pids/
build/ *.pid
*.seed
*.pid.lock
# Environment # Coverage directory used by tools like istanbul
.env coverage/
.env.local
# Docker
.dockerignore
# Temporary files
tmp/
temp/

307
JENKINS_SETUP.md Normal file
View File

@@ -0,0 +1,307 @@
# Jenkins Setup Instructions
## Prerequisites
### 1. Jenkins Installation
- Jenkins server with Docker support
- Required plugins:
- Pipeline Plugin
- Docker Pipeline Plugin
- NodeJS Plugin
- Git Plugin
- Blue Ocean (optional, for better UI)
- Coverage Plugin
- Test Results Analyzer
### 2. Prerequisites on Jenkins Agent
The pipeline now uses a simplified approach that doesn't require specific tool configurations in Jenkins Global Tools. Instead, ensure these tools are available on your Jenkins agent:
#### Required Tools
```bash
# Node.js 18+ (Required)
node --version # Should show v18.x.x or higher
npm --version # Should be available with Node.js
# Docker (Optional - for containerization stages)
docker --version
# Git (Usually pre-installed)
git --version
# Curl (Usually pre-installed)
curl --version
```
#### Installation Commands for Jenkins Agent
```bash
# Install Node.js 18 (Ubuntu/Debian)
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# Install Docker (Ubuntu/Debian)
sudo apt-get update
sudo apt-get install -y docker.io
sudo usermod -aG docker jenkins
sudo systemctl restart jenkins
# Install Trivy for security scanning (Optional)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
```
### 3. Alternative: Using Jenkins Global Tools (Advanced)
If you prefer to use Jenkins Global Tools Configuration:
#### Node.js Tool Configuration
- Go to **Manage Jenkins > Global Tool Configuration**
- **Name**: `nodejs` (keep it simple)
- **Version**: NodeJS 18.x.x
- **Install automatically**: ✅
Then update the Jenkinsfile to include:
```groovy
tools {
nodejs 'nodejs'
}
```
### 3. Required Software on Jenkins Agent
```bash
# Install Trivy for security scanning
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Install Docker Compose
curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
```
## Pipeline Features
### ✅ Current Implementation
1. **Multi-stage Pipeline**: Organized stages for better visibility
2. **Parallel Execution**: Dependencies and tests run in parallel
3. **Environment Detection**: Different actions for different branches
4. **Docker Integration**: Build and scan container images
5. **Security Scanning**: Trivy integration for vulnerability scanning
6. **Artifact Management**: Archive build artifacts and reports
7. **Integration Testing**: Health checks and API testing
8. **Branch-based Deployment**: Different environments for different branches
### 🔄 Pipeline Stages
#### 1. **Checkout**
- Clone repository
- Get Git commit information
#### 2. **Environment Info**
- Display tool versions
- Build information
#### 3. **Install Dependencies** (Parallel)
- Backend: `npm ci`
- Frontend: `npm ci`
#### 4. **Code Quality & Security** (Parallel)
- Linting for both frontend and backend
- Security audit with `npm audit`
#### 5. **Test** (Parallel)
- Backend unit tests
- Frontend tests with coverage
#### 6. **Build** (Parallel)
- Build backend (if build script exists)
- Build frontend React application
#### 7. **Docker Build** (Conditional)
- Build Docker images for both services
- Only on main/development/release branches
#### 8. **Docker Security Scan** (Conditional)
- Scan images with Trivy
- Generate security reports
#### 9. **Integration Tests** (Conditional)
- Start services with docker-compose
- Run health checks and API tests
#### 10. **Deployment** (Branch-specific)
- **Development**: Auto-deploy to dev environment
- **Release**: Deploy to staging
- **Main**: Manual approval for production
## Branch Strategy
### 🌿 Development Branch
- Automatic deployment to development environment
- Full testing pipeline
- Security scanning
### 🚀 Release Branch
- Deploy to staging environment
- Full security validation
- Performance testing ready
### 📦 Main Branch
- Production deployment with manual approval
- Complete security validation
- Artifact archival
## Security Features
### 🔒 Implemented Security Checks
1. **Dependency Scanning**: `npm audit` for known vulnerabilities
2. **Container Scanning**: Trivy for Docker image vulnerabilities
3. **Code Quality**: Linting for code standards
4. **Security Reports**: JSON reports archived as artifacts
### 🛡️ Future Security Enhancements
- SAST (Static Application Security Testing)
- DAST (Dynamic Application Security Testing)
- Infrastructure as Code scanning
- Secret scanning
- License compliance checking
## Environment Variables
### Required Environment Variables
```bash
# Docker Registry (update in Jenkinsfile)
REGISTRY=your-docker-registry.com
# Notification settings
SLACK_WEBHOOK=your-slack-webhook
EMAIL_RECIPIENTS=team@company.com
```
## Usage
### 1. Create Pipeline Job
1. Go to Jenkins Dashboard
2. Click "New Item"
3. Choose "Pipeline"
4. Configure SCM to point to your repository
5. Set script path to `Jenkinsfile`
### 2. Configure Webhooks
Add webhook in GitHub repository settings:
- URL: `http://your-jenkins-server/github-webhook/`
- Events: Push, Pull Request
### 3. First Run
- The pipeline will auto-detect the branch
- Development branch triggers full pipeline with dev deployment
- Main branch requires manual approval for production
## Monitoring & Notifications
### 📊 Build Artifacts
- Test results and coverage reports
- Security scan reports
- Built frontend application
- Docker image information
### 📧 Notifications
- Success/failure notifications
- Security alert notifications
- Deployment confirmations
## Troubleshooting
### Common Issues
#### 1. **Tool not found errors**
```
Tool type "nodejs" does not have an install of "NodeJS-18" configured
```
**Solution**:
- Current Jenkinsfile doesn't require tool configuration
- Ensure Node.js is installed on Jenkins agent: `node --version`
- If needed, install with: `curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - && sudo apt-get install -y nodejs`
#### 2. **Node.js not available**
```
Node.js is not available. Please install Node.js 18+ on the Jenkins agent.
```
**Solution**: Install Node.js on the Jenkins agent machine:
```bash
# Ubuntu/Debian
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# CentOS/RHEL
curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -
sudo yum install -y nodejs
```
#### 2. **Missing FilePath context in post actions**
```
Required context class hudson.FilePath is missing
```
**Solution**: Already fixed in current Jenkinsfile with proper script blocks
#### 3. **Docker permission denied**
```
docker: Got permission denied while trying to connect to the Docker daemon socket
```
**Solution**: Add Jenkins user to docker group:
```bash
sudo usermod -aG docker jenkins
sudo systemctl restart jenkins
```
#### 4. **Trivy not found**
```
trivy: command not found
```
**Solution**: Install Trivy on Jenkins agent:
```bash
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
```
#### 5. **Port conflicts during integration tests**
```
curl: (7) Failed to connect to localhost port 3001
```
**Solution**: Ensure ports 3000, 3001, 80 are available on Jenkins agent
### Debug Commands
```bash
# Check Jenkins agent tools
which node npm docker trivy
# Verify Docker access
docker ps
# Test repository access
git clone https://github.com/K0ngS3ng/DevSecOpsApp.git
# Check tool configurations in Jenkins
curl -u admin:password http://jenkins-url/manage/configureTools/
```
### Pipeline Configuration Examples
#### Minimal Configuration (No Docker)
If Docker is not available, the pipeline will gracefully skip Docker-related stages:
```groovy
// Pipeline will automatically skip Docker stages if tools are not available
// Error handling is built-in for all Docker operations
```
#### Custom Tool Names
If you have different tool names configured:
```groovy
tools {
nodejs 'Node18' // Your custom NodeJS name
dockerTool 'MyDocker' // Your custom Docker name
}
```
## Next Steps
1. Configure actual deployment environments
2. Add more comprehensive tests
3. Integrate with monitoring tools
4. Set up notification channels
5. Add performance testing stages

131
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,131 @@
pipeline {
// 1. Run all heavy lifting (builds) on the dedicated Agent
agent { label 'jenkins-agent' }
environment {
// --- REGISTRY & REPOSITORY FIXES ---
REGISTRY_URL = 'registry.digitalocean.com/devsecops-lab'
REPO_NAME = 'core' // Single repository for Free Tier
// --- TAGS ---
BACKEND_TAG = "backend-${env.BRANCH_NAME}-${env.BUILD_NUMBER}"
FRONTEND_TAG = "frontend-${env.BRANCH_NAME}-${env.BUILD_NUMBER}"
// --- DEPLOYMENT TARGET ---
DEPLOY_HOST = 'gitea.kongseng.in'
DEPLOY_USER = 'root'
}
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Install Dependencies') {
steps {
echo "Installing dependencies for ${env.BRANCH_NAME} branch..."
// Assuming Node is installed on the agent
dir('backend') { sh 'npm install' }
dir('frontend') { sh 'npm install' }
}
}
stage('Build Docker Images') {
steps {
script {
echo "Building Backend & Frontend Images..."
sh "docker build -t ${REGISTRY_URL}/${REPO_NAME}:${BACKEND_TAG} ./backend"
sh "docker build -t ${REGISTRY_URL}/${REPO_NAME}:${FRONTEND_TAG} ./frontend"
}
}
}
stage('Push to Registry') {
steps {
withCredentials([string(credentialsId: 'do-registry-token', variable: 'DO_TOKEN')]) {
script {
echo "Logging into DigitalOcean Registry..."
// Clean login for robustness
sh 'rm -f ~/.docker/config.json'
sh 'echo $DO_TOKEN | docker login registry.digitalocean.com -u token --password-stdin'
echo "Pushing images..."
sh "docker push ${REGISTRY_URL}/${REPO_NAME}:${BACKEND_TAG}"
sh "docker push ${REGISTRY_URL}/${REPO_NAME}:${FRONTEND_TAG}"
sh 'docker logout registry.digitalocean.com'
}
}
}
}
stage('Remote Deploy') {
steps {
script {
echo "--- REMOTE DEPLOYMENT STARTED ---"
// 1. Define Dynamic Ports and Names
def backPort = "3000"
def frontPort = "4000"
if (env.BRANCH_NAME == 'Dev') { backPort = "3001"; frontPort = "4001"; }
else if (env.BRANCH_NAME == 'Release') { backPort = "3002"; frontPort = "4002"; }
else if (env.BRANCH_NAME == 'main') { backPort = "3003"; frontPort = "4003"; }
def backName = "backend-${env.BRANCH_NAME}"
def frontName = "frontend-${env.BRANCH_NAME}"
// 2. Define SSH Command using the specific deploy key
def remote = "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa_deploy ${DEPLOY_USER}@${DEPLOY_HOST}"
// 3. Authenticate and Deploy BOTH Containers
withCredentials([string(credentialsId: 'do-registry-token', variable: 'DO_TOKEN')]) {
// Remote Login (Gitea Server needs to pull)
sh "${remote} 'echo ${DO_TOKEN} | docker login registry.digitalocean.com -u token --password-stdin'"
// --- BACKEND DEPLOYMENT (Primary Service) ---
echo "Deploying Backend to Port ${backPort}..."
sh "${remote} 'docker pull ${REGISTRY_URL}/${REPO_NAME}:${BACKEND_TAG}'"
sh "${remote} 'docker stop ${backName} || true'"
sh "${remote} 'docker rm ${backName} || true'"
sh """
${remote} 'docker run -d \
--name ${backName} \
--restart always \
-p ${backPort}:3001 \
${REGISTRY_URL}/${REPO_NAME}:${BACKEND_TAG}'
"""
// --- FRONTEND DEPLOYMENT (Secondary Service) ---
echo "Deploying Frontend to Port ${frontPort}..."
sh "${remote} 'docker pull ${REGISTRY_URL}/${REPO_NAME}:${FRONTEND_TAG}'"
sh "${remote} 'docker stop ${frontName} || true'"
sh "${remote} 'docker rm ${frontName} || true'"
sh """
${remote} 'docker run -d \
--name ${frontName} \
--restart always \
-p ${frontPort}:80 \
${REGISTRY_URL}/${REPO_NAME}:${FRONTEND_TAG}'
"""
sh 'docker logout registry.digitalocean.com'
echo "Deployment Complete! Backend: http://${DEPLOY_HOST}:${backPort}, Frontend: http://${DEPLOY_HOST}:${frontPort}"
}
}
}
}
}
post {
always {
// Cleanup: Delete source code and clean Docker cache on the Agent
echo 'Cleaning Agent workspace...'
deleteDir()
sh 'docker system prune -f'
}
}
}

46
PR_DESCRIPTION.md Normal file
View File

@@ -0,0 +1,46 @@
# 🔒 Feature: Enhanced TruffleHog Secret Detection Testing
## Purpose
This feature branch contains **intentional test secrets** to validate our enhanced TruffleHog configuration and ensure proper secret detection in our DevSecOps CI/CD pipeline.
## Changes Made
- ✅ Added comprehensive test secrets to `backend/server.js`:
- AWS Access Key & Secret Access Key
- GitHub Personal Access Token
- Database Password
- JWT Secret Key
- Stripe Secret Key
- SendGrid API Key
- Slack Webhook URL
- MongoDB Connection String
## Expected DevSecOps Pipeline Behavior
🚨 **This PR should FAIL the Jenkins build** due to secret detection:
1. **Jenkins Pipeline Trigger**: PR creation should trigger multibranch pipeline
2. **Secret Scan Stage**: TruffleHog should scan local workspace (`trufflehog filesystem . --fail`)
3. **Multiple Secret Detection**: Should detect 8+ different types of secrets
4. **Build Failure**: Pipeline should fail at "Secret Scan" stage
5. **Security Gate**: PR should be blocked from merging
## DevSecOps Learning Objectives
This feature validates our security controls:
- ✅ Local workspace scanning (vs remote GitHub scanning)
- ✅ Multiple secret pattern detection
- ✅ CI/CD security gate enforcement
- ✅ Automated security failure notifications
## Merge Strategy
**Target Branch**: `development`
## Post-Validation Steps
After confirming TruffleHog detection works:
1. Remove all test secrets from `server.js`
2. Update PR to pass security scan
3. Merge clean code into development
4. Document security scanning success
---
**⚠️ SECURITY WARNING: This PR contains test secrets and should NOT be merged until all secrets are removed!**
**📚 DevSecOps Learning**: This demonstrates "shift-left" security practices by catching secrets early in the development cycle.

View File

@@ -1,43 +1,6 @@
# DevSecOps-Lab Final documentation update
Updated project with latest improvements
Sample Application for Learning DevSecOps ## Testing Pipeline Integration
This update tests the enhanced Jenkins pipeline with TruffleHog secret scanning.
## Sample Web Page Date: Mon Sep 15 00:43:23 IST 2025
A simple, responsive web application built with HTML, CSS, and JavaScript.
### Features
- Responsive design for all devices
- Modern UI with gradient backgrounds
- Smooth navigation and scrolling
- Contact form functionality
- Service cards showcase
### Project Structure
```
.
├── index.html # Main HTML file
├── styles.css # CSS styling
├── script.js # JavaScript functionality
└── README.md # This file
```
### Quick Start
1. Open `index.html` in your browser
2. Navigate through sections using the navigation menu
3. Click "Get Started" button to see the CTA functionality
4. Fill in the contact form to test form submission
### Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
### License
MIT License - Feel free to use this project as a starting point for your own web applications.

34
backend/Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# Simple Node.js backend Dockerfile
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Create a non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodeapp -u 1001
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && npm cache clean --force
# Copy all application files
COPY . ./
# Create logs directory and set permissions
RUN mkdir -p logs && chown -R nodeapp:nodejs /app
# Switch to non-root user
USER nodeapp
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
# Expose port 3001 for backend
EXPOSE 3001
# Start the application
CMD ["node", "server.js"]

868
backend/package-lock.json generated Normal file
View File

@@ -0,0 +1,868 @@
{
"name": "todo-backend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "todo-backend",
"version": "1.0.0",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}

22
backend/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "todo-backend",
"version": "1.0.0",
"description": "Simple Backend API for Todo Application",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node server.js",
"test": "echo \"No tests specified\" && exit 0",
"build": "echo \"Backend build completed\" && exit 0",
"lint": "echo \"Linting not configured yet\" && exit 0",
"security": "npm audit --audit-level=high"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1"
},
"keywords": ["todo", "api", "express"],
"author": "DevOps Learner",
"license": "MIT"
}

193
backend/server.js Normal file
View File

@@ -0,0 +1,193 @@
const express = require('express');
const cors = require('cors');
require('dotenv').config();
// Load secrets from environment variables (never hardcode secrets!)
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const DATABASE_PASSWORD = process.env.DATABASE_PASSWORD;
const JWT_SECRET = process.env.JWT_SECRET;
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
const SENDGRID_API_KEY = process.env.SENDGRID_API_KEY;
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
const MONGODB_CONNECTION = process.env.MONGODB_CONNECTION;
const TWITTER_API_KEY = process.env.TWITTER_API_KEY;
// No-op variable used only to trigger CI/CD pipelines on small pushes
const TRIGGER_PIPELINE_VAR = 'trigger-20251130';
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(cors());
app.use(express.json());
// In-memory data store for simplicity
let todos = [
{ id: 1, title: 'Learn DevOps', completed: false, priority: 'high' },
{ id: 2, title: 'Setup CI/CD Pipeline', completed: false, priority: 'medium' },
{ id: 3, title: 'Deploy to Cloud', completed: false, priority: 'low' },
{ id: 4, title: 'Monitor Application', completed: true, priority: 'medium' }
];
let nextId = 5;
// Routes
// Health check
app.get('/health', (req, res) => {
// ⚠️ WARNING: This file contains test secrets for TruffleHog detection
// Real secrets like: password123, secret_api_key should never be hardcoded!
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
message: 'Backend server is running!'
});
});
// Get all todos
app.get('/api/todos', (req, res) => {
res.json({
success: true,
data: todos,
count: todos.length
});
});
// Get single todo
app.get('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const todo = todos.find(t => t.id === id);
if (!todo) {
return res.status(404).json({
success: false,
message: 'Todo not found'
});
}
res.json({
success: true,
data: todo
});
});
// Create new todo
app.post('/api/todos', (req, res) => {
const { title, priority = 'medium' } = req.body;
if (!title) {
return res.status(400).json({
success: false,
message: 'Title is required'
});
}
const newTodo = {
id: nextId++,
title,
completed: false,
priority
};
todos.push(newTodo);
res.status(201).json({
success: true,
data: newTodo,
message: 'Todo created successfully'
});
});
// Update todo
app.put('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const todoIndex = todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
return res.status(404).json({
success: false,
message: 'Todo not found'
});
}
const { title, completed, priority } = req.body;
if (title !== undefined) todos[todoIndex].title = title;
if (completed !== undefined) todos[todoIndex].completed = completed;
if (priority !== undefined) todos[todoIndex].priority = priority;
res.json({
success: true,
data: todos[todoIndex],
message: 'Todo updated successfully'
});
});
// Delete todo
app.delete('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const todoIndex = todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
return res.status(404).json({
success: false,
message: 'Todo not found'
});
}
todos.splice(todoIndex, 1);
res.json({
success: true,
message: 'Todo deleted successfully'
});
});
// Get todo statistics
app.get('/api/stats', (req, res) => {
const stats = {
total: todos.length,
completed: todos.filter(t => t.completed).length,
pending: todos.filter(t => !t.completed).length,
byPriority: {
high: todos.filter(t => t.priority === 'high').length,
medium: todos.filter(t => t.priority === 'medium').length,
low: todos.filter(t => t.priority === 'low').length
}
};
res.json({
success: true,
data: stats
});
});
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: 'Endpoint not found'
});
});
// Error handler
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({
success: false,
message: 'Internal server error'
});
});
// Start server
app.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 Backend server running on port ${PORT}`);
console.log(`📊 Health check: http://localhost:${PORT}/health`);
console.log(`📝 API endpoints: http://localhost:${PORT}/api/todos`);
console.log(`📈 Stats: http://localhost:${PORT}/api/stats`);
});
module.exports = app;

45
docker-compose.yml Normal file
View File

@@ -0,0 +1,45 @@
version: '3.8'
services:
# Backend API
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: todo-backend
restart: unless-stopped
environment:
NODE_ENV: production
PORT: 3001
ports:
- "3001:3001"
networks:
- todo-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/health"]
interval: 30s
timeout: 10s
retries: 3
# Frontend React App
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: todo-frontend
restart: unless-stopped
ports:
- "80:80"
depends_on:
- backend
networks:
- todo-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/health"]
interval: 30s
timeout: 10s
retries: 3
networks:
todo-network:
driver: bridge

47
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,47 @@
# Multi-stage build for React frontend
FROM node:18-alpine AS build
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY public/ ./public/
COPY src/ ./src/
# Build the application
RUN npm run build
# Production stage with Nginx
FROM nginx:alpine AS production
# Copy custom nginx config
COPY nginx.conf /etc/nginx/nginx.conf
# Copy built React app
COPY --from=build /app/build /usr/share/nginx/html
# Create a non-root user for nginx
RUN addgroup -g 1001 -S nginxgroup && \
adduser -S nginxuser -u 1001 -G nginxgroup
# Change ownership of nginx directories
RUN chown -R nginxuser:nginxgroup /var/cache/nginx && \
chown -R nginxuser:nginxgroup /var/log/nginx && \
chown -R nginxuser:nginxgroup /etc/nginx/conf.d && \
chown -R nginxuser:nginxgroup /usr/share/nginx/html
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

88
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,88 @@
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Handle React Router
location / {
try_files $uri $uri/ /index.html;
}
# Proxy API calls to backend
location /api {
# Use Docker bridge gateway so container can reach host's backend
# 172.17.0.1 is the common Docker bridge gateway; adjust if different
proxy_pass http://172.17.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}

19477
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
frontend/package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "todo-frontend",
"version": "1.0.0",
"description": "Simple React frontend for Todo Application",
"private": true,
"dependencies": {
"@types/node": "^16.18.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^4.9.5",
"web-vitals": "^3.4.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"react-scripts": "^5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --coverage --watchAll=false",
"test:watch": "react-scripts test",
"eject": "react-scripts eject",
"lint": "echo \"Linting not configured yet\" && exit 0",
"security": "npm audit --audit-level=high"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://backend:3001"
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="DevSecOps Todo Application - A comprehensive todo app for learning DevSecOps practices"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>DevSecOps Todo App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

331
frontend/src/App.css Normal file
View File

@@ -0,0 +1,331 @@
.App {
text-align: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.App-header {
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 2rem 0;
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.App-header h1 {
margin: 0;
font-size: 2.5rem;
font-weight: 700;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.App-header p {
margin: 0.5rem 0 0 0;
font-size: 1.2rem;
opacity: 0.9;
}
.App-main {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
display: grid;
gap: 2rem;
}
.loading {
color: white;
font-size: 1.5rem;
padding: 2rem;
}
.error-message {
background-color: #ff4757;
color: white;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Statistics Section */
.stats-section {
background-color: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.stats-section h2 {
margin-top: 0;
color: #333;
font-size: 1.8rem;
margin-bottom: 1.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
}
.stat-card {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 1.5rem;
border-radius: 12px;
text-align: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: transform 0.2s ease;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
/* Todo Section */
.todo-section {
background-color: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.todo-section h2 {
margin-top: 0;
color: #333;
font-size: 1.8rem;
margin-bottom: 1.5rem;
}
.add-todo-form {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.todo-input {
flex: 1;
min-width: 200px;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s ease;
}
.todo-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.priority-select {
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
background-color: white;
cursor: pointer;
transition: border-color 0.2s ease;
}
.priority-select:focus {
outline: none;
border-color: #667eea;
}
.add-button {
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.add-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3);
}
.no-todos {
color: #666;
font-style: italic;
padding: 2rem;
background-color: #f8f9fa;
border-radius: 8px;
margin: 1rem 0;
}
.todo-list {
display: grid;
gap: 1rem;
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: white;
border: 2px solid #e0e0e0;
border-radius: 12px;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.todo-item:hover {
border-color: #667eea;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.todo-item.completed {
opacity: 0.7;
background-color: #f8f9fa;
}
.todo-content {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
}
.todo-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
}
.todo-title {
font-size: 1rem;
color: #333;
flex: 1;
text-align: left;
}
.todo-item.completed .todo-title {
text-decoration: line-through;
color: #666;
}
.todo-priority {
color: white;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
}
.delete-button {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 6px;
transition: background-color 0.2s ease;
}
.delete-button:hover {
background-color: #ffebee;
}
/* Info Section */
.info-section {
background-color: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.info-section h3 {
margin-top: 0;
color: #333;
font-size: 1.5rem;
margin-bottom: 1rem;
}
.info-section ul {
list-style: none;
padding: 0;
margin: 0;
}
.info-section li {
padding: 0.5rem 0;
color: #555;
font-size: 1rem;
border-bottom: 1px solid #eee;
}
.info-section li:last-child {
border-bottom: none;
}
/* Responsive Design */
@media (max-width: 768px) {
.App-main {
padding: 1rem;
gap: 1rem;
}
.App-header h1 {
font-size: 2rem;
}
.add-todo-form {
flex-direction: column;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.todo-content {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.todo-priority {
align-self: flex-start;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.App-header {
padding: 1rem 0;
}
.App-header h1 {
font-size: 1.8rem;
}
.App-header p {
font-size: 1rem;
}
}

255
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,255 @@
import React, { useState, useEffect } from 'react';
import './App.css';
interface Todo {
id: number;
title: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
}
interface Stats {
total: number;
completed: number;
pending: number;
byPriority: {
high: number;
medium: number;
low: number;
};
}
const API_BASE = process.env.REACT_APP_API_URL || '/api';
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
const [stats, setStats] = useState<Stats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [newTodoTitle, setNewTodoTitle] = useState('');
const [newTodoPriority, setNewTodoPriority] = useState<'low' | 'medium' | 'high'>('medium');
useEffect(() => {
fetchTodos();
fetchStats();
}, []);
const fetchTodos = async () => {
try {
const response = await fetch(`${API_BASE}/todos`);
const data = await response.json();
if (data.success) {
setTodos(data.data);
} else {
setError('Failed to fetch todos');
}
} catch (err) {
setError('Failed to connect to server');
console.error('Error fetching todos:', err);
} finally {
setLoading(false);
}
};
const fetchStats = async () => {
try {
const response = await fetch(`${API_BASE}/stats`);
const data = await response.json();
if (data.success) {
setStats(data.data);
}
} catch (err) {
console.error('Error fetching stats:', err);
}
};
const addTodo = async (e: React.FormEvent) => {
e.preventDefault();
if (!newTodoTitle.trim()) return;
try {
const response = await fetch(`${API_BASE}/todos`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: newTodoTitle,
priority: newTodoPriority,
}),
});
const data = await response.json();
if (data.success) {
setTodos([...todos, data.data]);
setNewTodoTitle('');
fetchStats();
} else {
setError('Failed to add todo');
}
} catch (err) {
setError('Failed to add todo');
console.error('Error adding todo:', err);
}
};
const toggleTodo = async (id: number) => {
const todo = todos.find(t => t.id === id);
if (!todo) return;
try {
const response = await fetch(`${API_BASE}/todos/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
completed: !todo.completed,
}),
});
const data = await response.json();
if (data.success) {
setTodos(todos.map(t => t.id === id ? data.data : t));
fetchStats();
}
} catch (err) {
console.error('Error updating todo:', err);
}
};
const deleteTodo = async (id: number) => {
try {
const response = await fetch(`${API_BASE}/todos/${id}`, {
method: 'DELETE',
});
const data = await response.json();
if (data.success) {
setTodos(todos.filter(t => t.id !== id));
fetchStats();
}
} catch (err) {
console.error('Error deleting todo:', err);
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return '#ff4444';
case 'medium': return '#ff8800';
case 'low': return '#44aa44';
default: return '#666';
}
};
if (loading) return <div className="loading">Loading...</div>;
return (
<div className="App">
<header className="App-header">
<h1>🚀 DevSecOps Todo Application</h1>
<p>A simple todo app for learning DevOps practices</p>
</header>
<main className="App-main">
{error && <div className="error-message">{error}</div>}
<div className="stats-section">
<h2>📊 Statistics</h2>
{stats && (
<div className="stats-grid">
<div className="stat-card">
<div className="stat-value">{stats.total}</div>
<div className="stat-label">Total</div>
</div>
<div className="stat-card">
<div className="stat-value">{stats.completed}</div>
<div className="stat-label">Completed</div>
</div>
<div className="stat-card">
<div className="stat-value">{stats.pending}</div>
<div className="stat-label">Pending</div>
</div>
<div className="stat-card">
<div className="stat-value">{stats.byPriority.high}</div>
<div className="stat-label">High Priority</div>
</div>
</div>
)}
</div>
<div className="todo-section">
<h2>📝 Add New Todo</h2>
<form onSubmit={addTodo} className="add-todo-form">
<input
type="text"
value={newTodoTitle}
onChange={(e) => setNewTodoTitle(e.target.value)}
placeholder="Enter todo title..."
className="todo-input"
/>
<select
value={newTodoPriority}
onChange={(e) => setNewTodoPriority(e.target.value as 'low' | 'medium' | 'high')}
className="priority-select"
>
<option value="low">Low Priority</option>
<option value="medium">Medium Priority</option>
<option value="high">High Priority</option>
</select>
<button type="submit" className="add-button">Add Todo</button>
</form>
<h2>📋 Todo List</h2>
{todos.length === 0 ? (
<p className="no-todos">No todos yet! Add one above.</p>
) : (
<div className="todo-list">
{todos.map(todo => (
<div key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<div className="todo-content">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="todo-checkbox"
/>
<span className="todo-title">{todo.title}</span>
<span
className="todo-priority"
style={{ backgroundColor: getPriorityColor(todo.priority) }}
>
{todo.priority}
</span>
</div>
<button
onClick={() => deleteTodo(todo.id)}
className="delete-button"
>
🗑
</button>
</div>
))}
</div>
)}
</div>
<div className="info-section">
<h3>🛠 DevOps Features Ready</h3>
<ul>
<li> Frontend & Backend Communication</li>
<li> RESTful API Endpoints</li>
<li> CORS Configuration</li>
<li> Error Handling</li>
<li><EFBFBD> Ready for CI/CD Pipeline</li>
<li>🐳 Ready for Dockerization</li>
<li>📊 Monitoring & Health Checks</li>
</ul>
</div>
</main>
</div>
);
}
export default App;

35
frontend/src/index.css Normal file
View File

@@ -0,0 +1,35 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
* {
box-sizing: border-box;
}
.MuiButton-root {
text-transform: none !important;
}
.error-message {
color: #d32f2f;
font-size: 0.875rem;
margin-top: 3px;
margin-left: 14px;
}
.success-message {
color: #2e7d32;
font-size: 0.875rem;
margin-top: 3px;
margin-left: 14px;
}

14
frontend/src/index.tsx Normal file
View File

@@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App.tsx';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

152
scripts/health-check.sh Executable file
View File

@@ -0,0 +1,152 @@
#!/bin/bash
# Health Check Script for DevSecOps Todo Application
# This script verifies that both frontend and backend services are running correctly
set -e # Exit on any error
echo "🏥 Starting health checks for DevSecOps Todo Application..."
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
BACKEND_URL="${BACKEND_URL:-http://localhost:3001}"
FRONTEND_URL="${FRONTEND_URL:-http://localhost:3000}"
MAX_RETRIES=5
RETRY_DELAY=2
# Function to check service health
check_service() {
local url=$1
local service_name=$2
local retries=0
echo "🔍 Checking $service_name at $url..."
while [ $retries -lt $MAX_RETRIES ]; do
if curl -f -s "$url/health" > /dev/null 2>&1; then
echo -e "${GREEN}$service_name is healthy${NC}"
return 0
else
retries=$((retries + 1))
echo -e "${YELLOW}$service_name not ready, attempt $retries/$MAX_RETRIES${NC}"
if [ $retries -lt $MAX_RETRIES ]; then
sleep $RETRY_DELAY
fi
fi
done
echo -e "${RED}$service_name health check failed after $MAX_RETRIES attempts${NC}"
return 1
}
# Function to check API endpoints
check_api_endpoints() {
echo "🔍 Testing API endpoints..."
# Test todos endpoint
if curl -f -s "$BACKEND_URL/api/todos" > /dev/null 2>&1; then
echo -e "${GREEN}✅ Todos API endpoint is working${NC}"
else
echo -e "${RED}❌ Todos API endpoint failed${NC}"
return 1
fi
# Test stats endpoint
if curl -f -s "$BACKEND_URL/api/stats" > /dev/null 2>&1; then
echo -e "${GREEN}✅ Stats API endpoint is working${NC}"
else
echo -e "${RED}❌ Stats API endpoint failed${NC}"
return 1
fi
return 0
}
# Function to test basic functionality
test_basic_functionality() {
echo "🧪 Testing basic functionality..."
# Test creating a todo
local response=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"title":"Health Check Todo","priority":"low"}' \
"$BACKEND_URL/api/todos")
if echo "$response" | grep -q "Health Check Todo"; then
echo -e "${GREEN}✅ Todo creation test passed${NC}"
else
echo -e "${RED}❌ Todo creation test failed${NC}"
return 1
fi
return 0
}
# Main health check execution
main() {
echo "=================================================="
echo "🚀 DevSecOps Todo Application Health Check"
echo "=================================================="
local failed=0
# Check backend health
if ! check_service "$BACKEND_URL" "Backend"; then
failed=1
fi
# Check frontend health (if running on different port)
if [ "$FRONTEND_URL" != "$BACKEND_URL" ]; then
if ! check_service "$FRONTEND_URL" "Frontend"; then
failed=1
fi
fi
# Test API endpoints
if ! check_api_endpoints; then
failed=1
fi
# Test basic functionality
if ! test_basic_functionality; then
failed=1
fi
echo "=================================================="
if [ $failed -eq 0 ]; then
echo -e "${GREEN}🎉 All health checks passed!${NC}"
echo -e "${GREEN}📊 Application is ready for use${NC}"
exit 0
else
echo -e "${RED}💥 Some health checks failed!${NC}"
echo -e "${RED}🔧 Please check the application logs${NC}"
exit 1
fi
}
# Show usage if help requested
if [[ "$1" == "--help" || "$1" == "-h" ]]; then
echo "DevSecOps Todo Application Health Check Script"
echo ""
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Environment Variables:"
echo " BACKEND_URL Backend service URL (default: http://localhost:3001)"
echo " FRONTEND_URL Frontend service URL (default: http://localhost:3000)"
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " $0 # Check local services"
echo " BACKEND_URL=http://api.example.com $0 # Check remote backend"
exit 0
fi
# Run main function
main "$@"

1
test.txt Normal file
View File

@@ -0,0 +1 @@
test

6
webhook-test.md Normal file
View File

@@ -0,0 +1,6 @@
# Webhook Test
This file is created to test GitHub webhook integration with Jenkins.
Created on: 14 September 2025
Purpose: Verify that changes trigger Jenkins builds automatically.