diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..c7f83c2 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,2 @@ +target +.git diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..938cc67 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,16 @@ +# build +FROM maven:3.9.6-eclipse-temurin-21 as build +WORKDIR /app +COPY pom.xml ./ +RUN mvn dependency:go-offline -B +COPY src ./src +RUN mvn clean package -DskipTests + +# runtime +FROM eclipse-temurin:21-jre-alpine +WORKDIR /app +COPY --from=build /app/target/burger-builder-backend-1.0.0.jar app.jar +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1 +CMD ["java", "-jar", "app.jar"] diff --git a/backend/SELECT b/backend/SELECT new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/main/java/com/burgerbuilder/controller/OrderController.java b/backend/src/main/java/com/burgerbuilder/controller/OrderController.java index 99ccba8..f458bc3 100644 --- a/backend/src/main/java/com/burgerbuilder/controller/OrderController.java +++ b/backend/src/main/java/com/burgerbuilder/controller/OrderController.java @@ -82,4 +82,4 @@ public ResponseEntity> getOrdersByStatus(@PathVariable Order.Orde public ResponseEntity handleOptions() { return ResponseEntity.ok().build(); } -} +} \ No newline at end of file diff --git a/backend/src/main/resources/application-azure.properties b/backend/src/main/resources/application-azure.properties index a5b5ae8..fdb5c17 100644 --- a/backend/src/main/resources/application-azure.properties +++ b/backend/src/main/resources/application-azure.properties @@ -1,23 +1,23 @@ # Server Configuration server.port=8080 -# Database Configuration (Azure SQL Database) -spring.datasource.url=${DB_URL:jdbc:sqlserver://${DB_HOST:your-server.database.windows.net}:${DB_PORT:1433};database=${DB_NAME:burgerbuilder};encrypt=true;trustServerCertificate=false;hostNameInCertificate=*.database.windows.net;loginTimeout=30;} -spring.datasource.username=${DB_USERNAME:your-username} -spring.datasource.password=${DB_PASSWORD:your-password} -spring.datasource.driver-class-name=${DB_DRIVER:com.microsoft.sqlserver.jdbc.SQLServerDriver} +# Database Configuration (PostgreSQL for Docker) +spring.datasource.url=${DB_URL:jdbc:postgresql://${DB_HOST:database}:${DB_PORT:5432}/${DB_NAME:burgerbuilder}} +spring.datasource.username=${DB_USERNAME:postgres} +spring.datasource.password=${DB_PASSWORD:YourStrong!Passw0rd} +spring.datasource.driver-class-name=${DB_DRIVER:org.postgresql.Driver} # JPA Configuration spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.SQLServerDialect +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.format_sql=true # Initialize database with schema and data spring.sql.init.mode=always spring.jpa.defer-datasource-initialization=true -spring.sql.init.schema-locations=classpath:schema.sql -spring.sql.init.data-locations=classpath:data.sql +spring.sql.init.schema-locations=classpath:schema-postgresql.sql +spring.sql.init.data-locations=classpath:data-postgresql.sql # CORS Configuration - Handled by WebMvcConfigurer in BurgerBuilderApplication.java @@ -29,3 +29,4 @@ logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE # Application Properties # CORS is now handled by CorsConfig.java + diff --git a/backend/src/main/resources/application-docker.properties b/backend/src/main/resources/application-docker.properties index bb5c05a..fdb5c17 100644 --- a/backend/src/main/resources/application-docker.properties +++ b/backend/src/main/resources/application-docker.properties @@ -30,4 +30,3 @@ logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE # Application Properties # CORS is now handled by CorsConfig.java - diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index da69d62..10f4f12 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,40 +1,39 @@ -# Server Configuration -server.port=8080 +# Server +server.port=${SERVER_PORT:8080} +server.address=0.0.0.0 +server.forward-headers-strategy=framework -# Database Configuration (Azure SQL) -spring.datasource.url=${DB_URL:jdbc:sqlserver://${DB_HOST:your-server.database.windows.net}:${DB_PORT:1433};database=${DB_NAME:burgerbuilder};encrypt=true;trustServerCertificate=false;hostNameInCertificate=*.database.windows.net;loginTimeout=30;} -spring.datasource.username=${DB_USERNAME:your-username} -spring.datasource.password=${DB_PASSWORD:your-password} -spring.datasource.driver-class-name=${DB_DRIVER:com.microsoft.sqlserver.jdbc.SQLServerDriver} +# Azure SQL +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:sqlserver://${DB_HOST}:${DB_PORT:1433};databaseName=${DB_NAME};encrypt=false;trustServerCertificate=true;loginTimeout=30} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:${DB_USERNAME}} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:${DB_PASSWORD}} +spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME:${DB_DRIVER:com.microsoft.sqlserver.jdbc.SQLServerDriver}} -# JPA Configuration -spring.jpa.hibernate.ddl-auto=update -spring.jpa.show-sql=true -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.SQLServerDialect -spring.jpa.properties.hibernate.format_sql=true +# JPA / Hibernate +spring.jpa.hibernate.ddl-auto=${JPA_DDL_AUTO:none} +spring.jpa.show-sql=${JPA_SHOW_SQL:false} +spring.jpa.properties.hibernate.format_sql=false -# Initialize database with schema and data -spring.sql.init.mode=always -spring.jpa.defer-datasource-initialization=true -spring.sql.init.schema-locations=classpath:schema.sql -spring.sql.init.data-locations=classpath:data.sql +# SQL init (schema.sql + data.sql) +spring.sql.init.mode=${SPRING_SQL_INIT_MODE:always} +spring.jpa.defer-datasource-initialization=${SPRING_JPA_DEFER_DATASOURCE_INITIALIZATION:true} +spring.sql.init.encoding=UTF-8 -# CORS Configuration -spring.web.cors.allowed-origins=http://localhost:3000,http://localhost:5173 -spring.web.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS -spring.web.cors.allowed-headers=* -spring.web.cors.allow-credentials=true +# CORS +spring.mvc.cors.allowed-origins=http://localhost:3000,http://localhost:5173,http://20.162.253.251 +spring.mvc.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS +spring.mvc.cors.allowed-headers=* +spring.mvc.cors.allow-credentials=true -# Logging Configuration -logging.level.com.burgerbuilder=DEBUG -logging.level.org.springframework.web=DEBUG -logging.level.org.hibernate.SQL=DEBUG -logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE - -# Application Properties -app.cors.allowed-origins=http://localhost:3000,http://localhost:5173 - -# Actuator Configuration +# Actuator management.endpoints.web.exposure.include=health,info -management.endpoint.health.show-details=when-authorized -management.health.defaults.enabled=true \ No newline at end of file +management.endpoint.health.probes.enabled=true +management.health.livenessState.enabled=true +management.health.readinessState.enabled=true +management.endpoints.web.base-path=/actuator + +# Logging +logging.level.com.burgerbuilder=INFO +logging.level.org.springframework.web=INFO +logging.level.org.hibernate.SQL=INFO +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=INFO diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql index 79de3d1..8082196 100644 --- a/backend/src/main/resources/data.sql +++ b/backend/src/main/resources/data.sql @@ -1,103 +1,99 @@ --- Sample data for Burger Builder application (SQL Server) --- Only insert data if table is empty (one-time initialization) - --- Check if ingredients table has data, if not insert default ingredients INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Classic Sesame Bun', 'buns', 1.50, 'Fresh sesame seed bun', '/images/buns/sesame-bun.jpg', 1, 1 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Classic Sesame Bun','buns',1.50,'Fresh sesame seed bun','/images/buns/sesame-bun.jpg',1,1 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Classic Sesame Bun'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Whole Wheat Bun', 'buns', 1.75, 'Healthy whole wheat bun', '/images/buns/whole-wheat-bun.jpg', 1, 2 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Whole Wheat Bun','buns',1.75,'Healthy whole wheat bun','/images/buns/whole-wheat-bun.jpg',1,2 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Whole Wheat Bun'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Brioche Bun', 'buns', 2.00, 'Rich and buttery brioche bun', '/images/buns/brioche-bun.jpg', 1, 3 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Brioche Bun','buns',2.00,'Rich and buttery brioche bun','/images/buns/brioche-bun.jpg',1,3 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Brioche Bun'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Gluten-Free Bun', 'buns', 2.25, 'Gluten-free alternative bun', '/images/buns/gluten-free-bun.jpg', 1, 4 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Gluten-Free Bun','buns',2.25,'Gluten-free alternative bun','/images/buns/gluten-free-bun.jpg',1,4 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Gluten-Free Bun'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Beef Patty', 'patties', 4.50, 'Juicy 100% beef patty', '/images/patties/beef-patty.jpg', 1, 1 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Beef Patty','patties',4.50,'Juicy 100% beef patty','/images/patties/beef-patty.jpg',1,1 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Beef Patty'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Chicken Breast', 'patties', 4.25, 'Grilled chicken breast', '/images/patties/chicken-breast.jpg', 1, 2 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Chicken Breast','patties',4.25,'Grilled chicken breast','/images/patties/chicken-breast.jpg',1,2 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Chicken Breast'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Turkey Patty', 'patties', 4.00, 'Lean turkey patty', '/images/patties/turkey-patty.jpg', 1, 3 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Turkey Patty','patties',4.00,'Lean turkey patty','/images/patties/turkey-patty.jpg',1,3 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Turkey Patty'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Veggie Patty', 'patties', 3.75, 'Plant-based veggie patty', '/images/patties/veggie-patty.jpg', 1, 4 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Veggie Patty','patties',3.75,'Plant-based veggie patty','/images/patties/veggie-patty.jpg',1,4 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Veggie Patty'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Salmon Patty', 'patties', 5.50, 'Fresh salmon patty', '/images/patties/salmon-patty.jpg', 1, 5 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Salmon Patty','patties',5.50,'Fresh salmon patty','/images/patties/salmon-patty.jpg',1,5 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Salmon Patty'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Lettuce', 'toppings', 0.50, 'Fresh crisp lettuce', '/images/toppings/lettuce.jpg', 1, 1 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Lettuce','toppings',0.50,'Fresh crisp lettuce','/images/toppings/lettuce.jpg',1,1 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Lettuce'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Tomato', 'toppings', 0.75, 'Fresh tomato slices', '/images/toppings/tomato.jpg', 1, 2 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Tomato','toppings',0.75,'Fresh tomato slices','/images/toppings/tomato.jpg',1,2 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Tomato'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Onion', 'toppings', 0.50, 'Raw or grilled onions', '/images/toppings/onion.jpg', 1, 3 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Onion','toppings',0.50,'Raw or grilled onions','/images/toppings/onion.jpg',1,3 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Onion'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Pickles', 'toppings', 0.50, 'Dill pickle slices', '/images/toppings/pickles.jpg', 1, 4 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Pickles','toppings',0.50,'Dill pickle slices','/images/toppings/pickles.jpg',1,4 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Pickles'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Mushrooms', 'toppings', 1.00, 'Sautéed mushrooms', '/images/toppings/mushrooms.jpg', 1, 5 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Mushrooms','toppings',1.00,'Sautéed mushrooms','/images/toppings/mushrooms.jpg',1,5 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Mushrooms'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Bacon', 'toppings', 1.50, 'Crispy bacon strips', '/images/toppings/bacon.jpg', 1, 6 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Bacon','toppings',1.50,'Crispy bacon strips','/images/toppings/bacon.jpg',1,6 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Bacon'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Avocado', 'toppings', 1.25, 'Fresh avocado slices', '/images/toppings/avocado.jpg', 1, 7 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Avocado','toppings',1.25,'Fresh avocado slices','/images/toppings/avocado.jpg',1,7 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Avocado'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Jalapeños', 'toppings', 0.75, 'Spicy jalapeño peppers', '/images/toppings/jalapenos.jpg', 1, 8 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Jalapeños','toppings',0.75,'Spicy jalapeño peppers','/images/toppings/jalapenos.jpg',1,8 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Jalapeños'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Ketchup', 'sauces', 0.25, 'Classic tomato ketchup', '/images/sauces/ketchup.jpg', 1, 1 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Ketchup','sauces',0.25,'Classic tomato ketchup','/images/sauces/ketchup.jpg',1,1 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Ketchup'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Mustard', 'sauces', 0.25, 'Yellow mustard', '/images/sauces/mustard.jpg', 1, 2 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Mustard','sauces',0.25,'Yellow mustard','/images/sauces/mustard.jpg',1,2 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Mustard'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Mayo', 'sauces', 0.25, 'Creamy mayonnaise', '/images/sauces/mayo.jpg', 1, 3 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Mayo','sauces',0.25,'Creamy mayonnaise','/images/sauces/mayo.jpg',1,3 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Mayo'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'BBQ Sauce', 'sauces', 0.50, 'Smoky BBQ sauce', '/images/sauces/bbq-sauce.jpg', 1, 4 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'BBQ Sauce','sauces',0.50,'Smoky BBQ sauce','/images/sauces/bbq-sauce.jpg',1,4 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='BBQ Sauce'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Ranch', 'sauces', 0.50, 'Creamy ranch dressing', '/images/sauces/ranch.jpg', 1, 5 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Ranch','sauces',0.50,'Creamy ranch dressing','/images/sauces/ranch.jpg',1,5 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Ranch'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Sriracha', 'sauces', 0.50, 'Spicy sriracha sauce', '/images/sauces/sriracha.jpg', 1, 6 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Sriracha','sauces',0.50,'Spicy sriracha sauce','/images/sauces/sriracha.jpg',1,6 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Sriracha'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Aioli', 'sauces', 0.75, 'Garlic aioli', '/images/sauces/aioli.jpg', 1, 7 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Aioli','sauces',0.75,'Garlic aioli','/images/sauces/aioli.jpg',1,7 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Aioli'); INSERT INTO ingredients (name, category, price, description, image_url, is_available, sort_order) -SELECT 'Buffalo Sauce', 'sauces', 0.50, 'Spicy buffalo sauce', '/images/sauces/buffalo-sauce.jpg', 1, 8 -WHERE NOT EXISTS (SELECT 1 FROM ingredients); +SELECT 'Buffalo Sauce','sauces',0.50,'Spicy buffalo sauce','/images/sauces/buffalo-sauce.jpg',1,8 +WHERE NOT EXISTS (SELECT 1 FROM dbo.ingredients WHERE name='Buffalo Sauce'); diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..83b641e --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,5 @@ +node_modules +dist +.git +.gitignore +coverage diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..39a7144 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,33 @@ +# ============================ +# 1️⃣ Build Stage +# ============================ +FROM node:20-alpine AS build +WORKDIR /app + +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# ============================ +# 2️⃣ Runtime Stage +# ============================ +FROM nginx:alpine + +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# ============================ +# 3️⃣ Dynamic config.js injection (fixed quoting) +# ============================ +RUN echo '#!/bin/sh' > /docker-entrypoint.sh && \ + echo "echo \"window.APP_CONFIG = { API_BASE_URL: '\${VITE_API_BASE_URL:-http://4.253.11.59/api}' };\" > /usr/share/nginx/html/config.js" >> /docker-entrypoint.sh && \ + echo 'nginx -g "daemon off;"' >> /docker-entrypoint.sh && \ + chmod +x /docker-entrypoint.sh + +EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1 + +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/frontend/index.html b/frontend/index.html index 072a57e..c4b36ae 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,8 @@ - frontend + Burger Builder +
diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 221a7e9..882d320 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -1,33 +1,39 @@ server { listen 80; server_name localhost; + root /usr/share/nginx/html; index index.html; - # Enable gzip compression gzip on; gzip_vary on; gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/x-javascript + application/xml+rss + application/json; + + location = /config.js { + add_header Cache-Control "no-store"; + try_files $uri =404; + } - # Serve static files location / { try_files $uri $uri/ /index.html; } - # Cache static assets location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; } - # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; - # Error pages error_page 404 /index.html; } - - diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index ce271c0..b5956b8 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,51 +1,93 @@ +// src/services/api.ts import axios from 'axios'; -import type { Ingredient, CartItem, Order, IngredientsResponse, IngredientCategory } from '../types'; +import type { + Ingredient, + CartItem, + Order, + IngredientsResponse, + IngredientCategory, +} from '../types'; -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'; +/** + * Resolve API base URL: + * 1) runtime (public/config.js): window.APP_CONFIG.API_BASE_URL ← preferred in prod + * 2) build-time env: import.meta.env.VITE_API_BASE_URL + * 3) local dev fallback + * + * NOTE: We keep '/api' in this base URL by convention. + * Example: http://4.253.11.59/api + */ +const API_BASE_URL = + (window as any).APP_CONFIG?.API_BASE_URL || + import.meta.env.VITE_API_BASE_URL || + 'http://localhost:8080/api'; -// Debug: Log the API URL to help with troubleshooting +// Debug logs (safe to keep; remove if you prefer) console.log('API_BASE_URL:', API_BASE_URL); console.log('VITE_API_BASE_URL env var:', import.meta.env.VITE_API_BASE_URL); +console.log('Runtime config:', (window as any).APP_CONFIG); +/** Axios instance. + * IMPORTANT: Use RELATIVE paths (no leading slash) in requests below + * so Axios joins them with baseURL correctly. + */ const apiClient = axios.create({ - baseURL: API_BASE_URL, - headers: { - 'Content-Type': 'application/json', - }, + baseURL: API_BASE_URL, // e.g., http://4.253.11.59/api + headers: { 'Content-Type': 'application/json' }, }); -// Ingredients APIs +/** Small helper: normalize any value to an array. + * - If v is already an array → return as-is + * - If v is a Spring Page-like object { content: [...] } → return content + * - Otherwise → [] + */ +const toArray = (v: unknown): T[] => { + if (Array.isArray(v)) return v as T[]; + const content = (v as any)?.content; + if (Array.isArray(content)) return content as T[]; + return []; +}; + +/* ========================= Ingredients ========================= */ + export const getIngredients = async (): Promise => { - const response = await apiClient.get('/api/ingredients'); - return response.data; + const res = await apiClient.get('ingredients'); + // Tolerate either array or wrapped payload (e.g., { items: [...] } or direct array) + if (Array.isArray(res.data)) return res.data as Ingredient[]; + if (Array.isArray(res.data?.items)) return res.data.items as Ingredient[]; + return res.data ?? []; }; -export const getIngredientsByCategory = async (category: IngredientCategory): Promise => { - const response = await apiClient.get(`/api/ingredients/${category}`); - return response.data; +export const getIngredientsByCategory = async ( + category: IngredientCategory +): Promise => { + const res = await apiClient.get(`ingredients/${category}`); + return toArray(res.data); }; -// Cart APIs +/* ============================ Cart ============================ */ + export const addToCart = async (item: { sessionId: string; ingredientId: number; quantity: number; burgerLayers?: { ingredientId: number; layerOrder: number; quantity: number }[]; }): Promise => { - const response = await apiClient.post('/api/cart/items', item); - return response.data; + const res = await apiClient.post('cart/items', item); + return res.data; }; export const getCart = async (sessionId: string): Promise => { - const response = await apiClient.get(`/api/cart/${sessionId}`); - return response.data; + const res = await apiClient.get(`cart/${sessionId}`); + return toArray(res.data); }; export const removeCartItem = async (itemId: number): Promise => { - await apiClient.delete(`/api/cart/items/${itemId}`); + await apiClient.delete(`cart/items/${itemId}`); }; -// Order APIs +/* =========================== Orders ============================ */ + export const createOrder = async (orderData: { sessionId: string; customerName: string; @@ -53,31 +95,35 @@ export const createOrder = async (orderData: { customerPhone: string; cartItemIds: number[]; }): Promise => { - const response = await apiClient.post('/api/orders', orderData); - return response.data; + const res = await apiClient.post('orders', orderData); + // Guard nested lists so downstream .map() never explodes + const order = res.data as any; + order.orderItems = toArray(order.orderItems); + order.layers = toArray(order.layers); + return order as Order; }; export const getOrder = async (orderId: string): Promise => { - const response = await apiClient.get(`/api/orders/${orderId}`); - return response.data; + const res = await apiClient.get(`orders/${orderId}`); + return res.data; }; -// Order History APIs +/* ======================= Order History ======================== */ + export const getOrderHistory = async (email?: string): Promise => { const params = email ? { email } : {}; - const response = await apiClient.get('/api/orders/history', { params }); - return response.data; + const res = await apiClient.get('orders/history', { params }); + return toArray(res.data); }; export const getOrdersByCustomerEmail = async (email: string): Promise => { - const response = await apiClient.get(`/api/orders/customer/${email}`); - return response.data; + const res = await apiClient.get(`orders/customer/${email}`); + return toArray(res.data); }; export const getOrdersBySession = async (sessionId: string): Promise => { - const response = await apiClient.get(`/api/orders/session/${sessionId}`); - return response.data; + const res = await apiClient.get(`orders/session/${sessionId}`); + return toArray(res.data); }; export default apiClient; - diff --git a/frontend/v.sh b/frontend/v.sh new file mode 100644 index 0000000..930964f --- /dev/null +++ b/frontend/v.sh @@ -0,0 +1,36 @@ +# =========[ setup ]========= +RG="rg-dev-Abdullah-Alotaibi" +FE="acafe-dev" # frontend +BE="acabe-dev" # backend +TAIL=200 + +# =========[ revisions ]========= +FE_REV=$(az containerapp revision list -g "$RG" -n "$FE" --query "[-1].name" -o tsv) +BE_REV=$(az containerapp revision list -g "$RG" -n "$BE" --query "[-1].name" -o tsv) + +# =========[ images running now (اختياري مفيد) ]========= +echo -e "\n=== Running images ===" +az containerapp show -g "$RG" -n "$FE" --query "properties.template.containers[].{name:name,image:image}" -o table +az containerapp show -g "$RG" -n "$BE" --query "properties.template.containers[].{name:name,image:image}" -o table + +# =========[ logs: application ]========= +echo -e "\n=== Frontend app logs ($FE:$FE_REV) ===" +az containerapp logs show -g "$RG" -n "$FE" --revision "$FE_REV" --tail $TAIL + +echo -e "\n=== Backend app logs ($BE:$BE_REV) ===" +az containerapp logs show -g "$RG" -n "$BE" --revision "$BE_REV" --tail $TAIL + +# =========[ logs: system ]========= +echo -e "\n=== Frontend system logs ===" +az containerapp logs show -g "$RG" -n "$FE" --type system --tail $TAIL + +echo -e "\n=== Backend system logs ===" +az containerapp logs show -g "$RG" -n "$BE" --type system --tail $TAIL + +# =========[ health via AGW ]========= +echo -e "\n=== Backend health via AGW ===" +PIP=$(az network public-ip show -g "$RG" -n pip-agw-dev --query ipAddress -o tsv) +curl -s "http://$PIP/api/health" || true + +echo -e "\n=== Done ===" +