diff --git a/Automations/updateBackend.sh b/Automations/updateBackend.sh new file mode 100755 index 000000000..5a797957d --- /dev/null +++ b/Automations/updateBackend.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Initializing variables +file_to_find="../backend/.env.docker" +alreadyUpdate=$(sed -n "4p" ../backend/.env.docker) +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +# Use curl to fetch the public IPv4 address from the metadata service +ipv4_address=$(curl -s http://169.254.169.254/latest/meta-data/public-ipv4) + +echo -e " ${GREEN}System Public Ipv4 address ${NC} : ${ipv4_address}" + +if [[ "${alreadyUpdate}" == "FRONTEND_URL=\"http://${ipv4_address}:5173\"" ]] +then + echo -e "${YELLOW}${file_to_find} file is already updated to the current host's Ipv4 ${NC}" + exit -1; +else + if [ -f ${file_to_find} ] + then + echo -e "${GREEN}${file_to_find}${NC} found.." + echo -e "${YELLOW}Configuring env variables in ${NC} ${file_to_find}" + sleep 7s; + sed -i -e "s|FRONTEND_URL.*|FRONTEND_URL=\"http://${ipv4_address}:5173\"|g" ${file_to_find} + echo -e "${GREEN}env variables configured..${NC}" + else + echo -e "${RED}ERROR : File not found..${NC}" + fi +fi diff --git a/Automations/updateFrontend.sh b/Automations/updateFrontend.sh new file mode 100755 index 000000000..f66464893 --- /dev/null +++ b/Automations/updateFrontend.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Initializing variables +file_to_find="../frontend/.env.docker" +alreadyUpdate=$(cat ../frontend/.env.docker) +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +# Use curl to fetch the public IPv4 address from the metadata service +ipv4_address=$(curl -s http://169.254.169.254/latest/meta-data/public-ipv4) + +echo -e " ${GREEN}System Public Ipv4 address ${NC} : ${ipv4_address}" + +if [[ "${alreadyUpdate}" == "VITE_API_PATH=\"http://${ipv4_address}:31100\"" ]] +then + echo -e "${YELLOW}${file_to_find} file is already updated to the current host's Ipv4 ${NC}" + exit -1; +else + if [ -f ${file_to_find} ] + then + echo -e "${GREEN}${file_to_find}${NC} found.." + echo -e "${YELLOW}Configuring env variables in ${NC} ${file_to_find}" + sleep 7s; + sed -i -e "s|VITE_API_PATH.*|VITE_API_PATH=\"http://${ipv4_address}:31100\"|g" ${file_to_find} + echo -e "${GREEN}env variables configured..${NC}" + else + echo -e "${RED}ERROR : File not found..${NC}" + fi +fi diff --git a/Automations/updatebackendnew.sh b/Automations/updatebackendnew.sh new file mode 100644 index 000000000..89afec2cb --- /dev/null +++ b/Automations/updatebackendnew.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Set the Instance ID and path to the .env file +INSTANCE_ID="i-0c7c9d3d4e8c3a012" + +# Retrieve the public IP address of the specified EC2 instance +ipv4_address=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID --query 'Reservations[0].Instances[0].PublicIpAddress' --output text) + +# Initializing variables +file_to_find="../backend/.env.docker" +alreadyUpdate=$(sed -n "4p" ../backend/.env.docker) +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +# Use curl to fetch the public IPv4 address from the metadata service + +echo -e " ${GREEN}System Public Ipv4 address ${NC} : ${ipv4_address}" + +if [[ "${alreadyUpdate}" == "FRONTEND_URL=\"http://${ipv4_address}:5173\"" ]] +then + echo -e "${YELLOW}${file_to_find} file is already updated to the current host's Ipv4 ${NC}" + exit -1; +else + if [ -f ${file_to_find} ] + then + echo -e "${GREEN}${file_to_find}${NC} found.." + echo -e "${YELLOW}Configuring env variables in ${NC} ${file_to_find}" + sleep 7s; + sed -i -e "s|FRONTEND_URL.*|FRONTEND_URL=\"http://${ipv4_address}:5173\"|g" ${file_to_find} + echo -e "${GREEN}env variables configured..${NC}" + else + echo -e "${RED}ERROR : File not found..${NC}" + fi +fi diff --git a/Automations/updatefrontendnew.sh b/Automations/updatefrontendnew.sh new file mode 100644 index 000000000..5c664fe9b --- /dev/null +++ b/Automations/updatefrontendnew.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Set the Instance ID and path to the .env file +INSTANCE_ID="i-0c7c9d3d4e8c3a012" + +# Retrieve the public IP address of the specified EC2 instance +ipv4_address=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID --query 'Reservations[0].Instances[0].PublicIpAddress' --output text) + +# Initializing variables +file_to_find="../frontend/.env.docker" +alreadyUpdate=$(cat ../frontend/.env.docker) +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +echo -e " ${GREEN}System Public Ipv4 address ${NC} : ${ipv4_address}" + +if [[ "${alreadyUpdate}" == "VITE_API_PATH=\"http://${ipv4_address}:31100\"" ]] +then + echo -e "${YELLOW}${file_to_find} file is already updated to the current host's Ipv4 ${NC}" + exit -1; +else + if [ -f ${file_to_find} ] + then + echo -e "${GREEN}${file_to_find}${NC} found.." + echo -e "${YELLOW}Configuring env variables in ${NC} ${file_to_find}" + sleep 7s; + sed -i -e "s|VITE_API_PATH.*|VITE_API_PATH=\"http://${ipv4_address}:31100\"|g" ${file_to_find} + echo -e "${GREEN}env variables configured..${NC}" + else + echo -e "${RED}ERROR : File not found..${NC}" + fi +fi diff --git a/GitOps/Jenkinsfile b/GitOps/Jenkinsfile new file mode 100644 index 000000000..63d11651d --- /dev/null +++ b/GitOps/Jenkinsfile @@ -0,0 +1,78 @@ +@Library('Shared') _ +pipeline { + agent {label 'Node'} + + parameters { + string(name: 'FRONTEND_DOCKER_TAG', defaultValue: '', description: 'Frontend Docker tag of the image built by the CI job') + string(name: 'BACKEND_DOCKER_TAG', defaultValue: '', description: 'Backend Docker tag of the image built by the CI job') + } + + stages { + stage("Workspace cleanup"){ + steps{ + script{ + cleanWs() + } + } + } + + stage('Git: Code Checkout') { + steps { + script{ + code_checkout("https://github.com/DevMadhup/wanderlust.git","devops") + } + } + } + + stage('Verify: Docker Image Tags') { + steps { + script{ + echo "FRONTEND_DOCKER_TAG: ${params.FRONTEND_DOCKER_TAG}" + echo "BACKEND_DOCKER_TAG: ${params.BACKEND_DOCKER_TAG}" + } + } + } + + + stage("Update: Kubernetes manifests"){ + steps{ + script{ + dir('kubernetes'){ + sh """ + sed -i -e 's/backend-wanderlust.*/backend-wanderlust:${params.BACKEND_DOCKER_TAG}/g' backend.yaml + """ + } + + dir('kubernetes'){ + sh """ + sed -i -e 's/frontend-wanderlust.*/frontend-wanderlust:${params.FRONTEND_DOCKER_TAG}/g' frontend.yaml + """ + } + + } + } + } + + stage("Git: Code update and push to GitHub"){ + steps{ + script{ + withCredentials([gitUsernamePassword(credentialsId: 'Github-cred', gitToolName: 'Default')]) { + sh ''' + echo "Checking repository status: " + git status + + echo "Adding changes to git: " + git add . + + echo "Commiting changes: " + git commit -m "Updated environment variables" + + echo "Pushing changes to github: " + git push https://github.com/DevMadhup/wanderlust.git devops + ''' + } + } + } + } + } +} diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 000000000..6f97484e8 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,119 @@ +@Library('Shared') _ +pipeline { + agent any + + environment{ + SONAR_HOME = tool "Sonar" + } + stages { + + stage("Workspace cleanup"){ + steps{ + script{ + cleanWs() + } + } + } + + stage('Git: Code Checkout') { + steps { + script{ + code_checkout("https://github.com/DevMadhup/wanderlust.git","devops") + } + } + } + + stage("OWASP: Dependency check"){ + steps{ + script{ + owasp_dependency() + } + } + post{ + success{ + archiveArtifacts artifacts: '**/dependency-check-report.xml', followSymlinks: false, onlyIfSuccessful: true + } + } + } + + stage("Trivy: Filesystem scan"){ + steps{ + script{ + trivy_scan() + } + } + } + + stage("SonarQube: Code Analysis"){ + steps{ + script{ + sonarqube_analysis("Sonar","wanderlust","wanderlust") + } + } + } + + stage("SonarQube: Code Quality Gates"){ + steps{ + script{ + sonarqube_code_quality() + } + } + } + + stage('Exporting environment variables') { + parallel{ + stage("Backend env setup"){ + steps { + script{ + dir("Automations"){ + sh "bash updateBackend.sh" + } + } + } + } + + stage("Frontend env setup"){ + steps { + script{ + dir("Automations"){ + sh "bash updateFrontend.sh" + } + } + } + } + } + } + + stage("Docker: Build Images"){ + steps{ + script{ + dir('backend'){ + docker_build("backend-wanderlust","test-image-donot-use","madhupdevops") + } + + dir('frontend'){ + docker_build("frontend-wanderlust","test-image-donot-use","madhupdevops") + } + } + } + } + + stage("Docker: Push to DockerHub"){ + steps{ + script{ + docker_push("backend-wanderlust","test-image-donot-use","madhupdevops") + docker_push("frontend-wanderlust","test-image-donot-use","madhupdevops") + } + } + } + } + + post{ + success{ + build job: "Wanderlust-CD", parameters: [ + string(name: 'FRONTEND_DOCKER_TAG', value: "test-image-donot-use"), + string(name: 'BACKEND_DOCKER_TAG', value: "test-image-donot-use") + ] + } + } +} diff --git a/README.md b/README.md index c064b393f..cc384cfa1 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,33 @@ _I'd love for you to make the most of this project - it's all about learning, he npm run dev ``` +### Setting up with Docker + +1. **Ensure Docker and Docker Compose are Installed** + +2. **Clone the Repository** + + ``` bash + + git clone https://github.com/{your-username}/wanderlust.git + ``` +3. **Navigate to the Project Directory** + + ```bash + + cd wanderlust + + ``` +4. **Update Environment Variables** - If you anticipate the IP address of the instance might change, update the `.env.sample` file with the new IP address. + +5. **Run Docker Compose** + + ```bash + + docker-compose up + ``` + This command will build the Docker images and start the containers for the backend and frontend, enabling you to access the Wanderlust application. + ## 🌟 Ready to Contribute? Kindly go through [CONTRIBUTING.md](https://github.com/krishnaacharyaa/wanderlust/blob/main/.github/CONTRIBUTING.md) to understand everything from setup to contributing guidelines. diff --git a/backend/.env.docker b/backend/.env.docker new file mode 100644 index 000000000..4a2f307be --- /dev/null +++ b/backend/.env.docker @@ -0,0 +1,9 @@ +MONGODB_URI="mongodb://mongo-service/wanderlust" +REDIS_URL="redis://redis-service:6379" +PORT=5000 +ACCESS_COOKIE_MAXAGE=120000 +ACCESS_TOKEN_EXPIRES_IN='120s' +REFRESH_COOKIE_MAXAGE=120000 +REFRESH_TOKEN_EXPIRES_IN='120s' +JWT_SECRET=70dd8b38486eee723ce2505f6db06f1ee503fde5eb06fc04687191a0ed665f3f98776902d2c89f6b993b1c579a87fedaf584c693a106f7cbf16e8b4e67e9d6df +NODE_ENV=Development diff --git a/backend/.env.sample b/backend/.env.sample index 858cea92a..d351fae8a 100644 --- a/backend/.env.sample +++ b/backend/.env.sample @@ -1,2 +1,2 @@ MONGODB_URI="mongodb://127.0.0.1/wanderlust" -REDIS_URL="127.0.0.1:6379" \ No newline at end of file +REDIS_URL="127.0.0.1:6379" diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 000000000..cb78beb8f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,25 @@ +# Stage 1 - Build +FROM node:21 AS backend-builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +RUN npm run test + +# Stage 2 - Runtime +FROM node:21-slim + +WORKDIR /app + +COPY --from=backend-builder /app . + +COPY .env.docker .env + +EXPOSE 5000 + +# 🔥 ADD THIS LINE +CMD ["node", "server.js"] diff --git a/backend/server.js b/backend/server.js index 32c6e1d3a..54e5c7a7d 100644 --- a/backend/server.js +++ b/backend/server.js @@ -7,12 +7,19 @@ import { PORT } from './config/utils.js'; import authRouter from './routes/auth.js'; import postsRouter from './routes/posts.js'; import { connectToRedis } from './services/redis.js'; + const app = express(); const port = PORT || 5000; +// Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); -app.use(cors()); + +// ✅ CORS (keep open for now) +app.use(cors({ + origin: '*', +})); + app.use(cookieParser()); app.use(compression()); @@ -22,14 +29,16 @@ connectDB(); // Connect to redis connectToRedis(); -// API route +// ✅ API routes (already PERFECT) app.use('/api/posts', postsRouter); app.use('/api/auth', authRouter); +// Health check / root app.get('/', (req, res) => { res.send('Yay!! Backend of wanderlust app is now accessible'); }); +// Start server app.listen(port, () => { console.log(`Server is running on port ${port}`); }); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..50b6e9709 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: "3.8" +services: + mongodb: + container_name: mongo + image: mongo:latest + volumes: + - ./backend/data:/data + ports: + - "27017:27017" + + backend: + container_name: backend + build: ./backend + env_file: + - ./backend/.env.docker + ports: + - "5000:5000" + depends_on: + - mongodb + + frontend: + container_name: frontend + build: ./frontend + env_file: + - ./frontend/.env.docker + ports: + - "5173:5173" + + redis: + container_name: redis + restart: unless-stopped + image: redis:7.0.5-alpine + expose: + - 6379 + depends_on: + - mongodb + +volumes: + data: diff --git a/frontend/.env.sample b/frontend/.env.sample index 4a0ca5c61..1e8ba194f 100644 --- a/frontend/.env.sample +++ b/frontend/.env.sample @@ -1 +1 @@ -VITE_API_PATH="http://localhost:5000" \ No newline at end of file +VITE_API_PATH="http://localhost:5000" diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..aa7dd066a --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,23 @@ +# ------------------- Stage 1: Build ------------------- +FROM node:21 AS frontend-builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +RUN npm run build + +# ------------------- Stage 2: Serve ------------------- +FROM nginx:alpine + +COPY --from=frontend-builder /app/dist /usr/share/nginx/html + +# 🔥 ADD THIS +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 000000000..f22fbe2d2 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri /index.html; + } + + location /api/ { + proxy_pass http://backend-service; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cb6707954..4795511fc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -52,7 +52,7 @@ "ts-jest": "^29.1.1", "ts-jest-mock-import-meta": "^1.1.0", "ts-node": "^10.9.2", - "typescript": "^5.2.2", + "typescript": "^5.9.3", "vite": "^4.5.0" } }, @@ -9081,9 +9081,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "bin": { "tsc": "bin/tsc", diff --git a/frontend/package.json b/frontend/package.json index 4b7f5a4b4..c91b62c78 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -56,7 +56,7 @@ "ts-jest": "^29.1.1", "ts-jest-mock-import-meta": "^1.1.0", "ts-node": "^10.9.2", - "typescript": "^5.2.2", + "typescript": "^5.9.3", "vite": "^4.5.0" } } diff --git a/frontend/src/components/blog-feed.tsx b/frontend/src/components/blog-feed.tsx index 256062541..4fa6a90a9 100644 --- a/frontend/src/components/blog-feed.tsx +++ b/frontend/src/components/blog-feed.tsx @@ -21,7 +21,7 @@ export default function BlogFeed() { setLoading(true); axios - .get(import.meta.env.VITE_API_PATH + categoryEndpoint) + .get(categoryEndpoint) .then((response) => { setPosts(response.data); setLoading(false); @@ -33,7 +33,7 @@ export default function BlogFeed() { useEffect(() => { axios - .get(import.meta.env.VITE_API_PATH + '/api/posts/latest') + .get('/api/posts/latest') .then((response) => { setLatestPosts(response.data); }) diff --git a/frontend/src/pages/add-blog.tsx b/frontend/src/pages/add-blog.tsx index 47ce34bb8..2578c5a95 100644 --- a/frontend/src/pages/add-blog.tsx +++ b/frontend/src/pages/add-blog.tsx @@ -17,14 +17,12 @@ type FormData = { description: string; isFeaturedPost: boolean; }; + function AddBlog() { const [selectedImage, setSelectedImage] = useState(''); - - const handleImageSelect = (imageUrl: string) => { - setSelectedImage(imageUrl); - }; - const [modal, setmodal] = useState(false); + const navigate = useNavigate(); + const [formData, setFormData] = useState({ title: '', authorName: '', @@ -34,225 +32,172 @@ function AddBlog() { isFeaturedPost: false, }); - //checks the length of the categories array and if the category is already selected + const [isDarkMode, setIsDarkMode] = useState(null); + + useEffect(() => { + const storedTheme = localStorage.getItem('theme'); + setIsDarkMode(storedTheme === 'dark'); + }, []); + + const handleImageSelect = (imageUrl: string) => { + setSelectedImage(imageUrl); + }; + + const handleselector = () => { + setFormData((prev) => ({ + ...prev, + imageLink: selectedImage, + })); + setmodal(false); + }; + const isValidCategory = (category: string): boolean => { return formData.categories.length >= 3 && !formData.categories.includes(category); }; const handleInputChange = (e: ChangeEvent) => { const { name, value } = e.target; - setFormData({ ...formData, [name]: value }); + setFormData((prev) => ({ ...prev, [name]: value })); }; const handleCategoryClick = (category: string) => { if (isValidCategory(category)) return; if (formData.categories.includes(category)) { - setFormData({ - ...formData, - categories: formData.categories.filter((cat) => cat !== category), - }); + setFormData((prev) => ({ + ...prev, + categories: prev.categories.filter((cat) => cat !== category), + })); } else { - setFormData({ - ...formData, - categories: [...formData.categories, category], - }); + setFormData((prev) => ({ + ...prev, + categories: [...prev.categories, category], + })); } }; - const handleselector = () => { - setFormData({ - ...formData, - imageLink: selectedImage, - }); - setmodal(false); - }; const handleCheckboxChange = () => { - setFormData({ ...formData, isFeaturedPost: !formData.isFeaturedPost }); + setFormData((prev) => ({ + ...prev, + isFeaturedPost: !prev.isFeaturedPost, + })); }; + const validateFormData = () => { - if ( - !formData.title || - !formData.authorName || - !formData.imageLink || - !formData.description || - formData.categories.length === 0 - ) { - toast.error('All fields must be filled out.'); - return false; - } - const imageLinkRegex = /\.(jpg|jpeg|png|webp)$/i; - if (!imageLinkRegex.test(formData.imageLink)) { - toast.error('Image URL must end with .jpg, .jpeg, .webp or .png'); - return false; - } - if (formData.categories.length > 3) { - toast.error('Select up to three categories.'); - return false; - } + if (!formData.title) return toast.error('Title is required'), false; + if (!formData.authorName) return toast.error('Author is required'), false; + if (!formData.description) return toast.error('Description is required'), false; + if (formData.categories.length === 0) + return toast.error('Select at least 1 category'), false; return true; }; + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); - if (validateFormData()) { - try { - const response = await axios.post(import.meta.env.VITE_API_PATH + '/api/posts/', formData); - if (response.status === 200) { - toast.success('Blog post successfully created!'); - navigate('/'); - } else { - toast.error('Error: ' + response.data.message); - } - } catch (err: any) { - toast.error('Error: ' + err.message); + if (!validateFormData()) return; + + try { + const response = await axios.post('/api/posts', formData); + + if (response.status === 200 || response.status === 201) { + toast.success('Blog post successfully created!'); + navigate('/'); + } else { + toast.error('Error: ' + response.data.message); } + } catch (err: any) { + toast.error('Error: ' + err.message); } }; - const navigate = useNavigate(); - const [isDarkMode, setIsDarkMode] = useState(null); - useEffect(() => { - const storedTheme = localStorage.getItem('theme'); - setIsDarkMode(storedTheme === 'dark'); - }, []); - - function Asterisk() { - return *; - } return ( -
-
-
-
- navigate(-1)} - className="active:scale-click h-5 w-10" - /> -
-

- Create Blog -

+
+ {/* HEADER */} +
+
+ navigate(-1)} + className="h-5 w-10 cursor-pointer" + /> +

Create Blog

-
-
-
- -
- -
-
- Blog title -
- -
-
-
- Blog content -
-