diff --git a/frontend/package.json b/frontend/package.json index 0c16446..c3f35d0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,6 +3,8 @@ "version": "1.0.0", "dependencies": { "express": "^4.18.2", - "node-fetch": "^2.6.7" + "node-fetch": "^2.6.7", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express":"^5.0.1" } } diff --git a/frontend/public/app.js b/frontend/public/app.js index 01bf016..f6d150e 100644 --- a/frontend/public/app.js +++ b/frontend/public/app.js @@ -10,9 +10,11 @@ async function login() { body: JSON.stringify({ username, password }), }); - const data = await res.json(); - token = data.token; + if (!res.ok) return document.getElementById("output").textContent = "Login failed."; + token = (await res.json()).token; + if (!token) return document.getElementById("output").textContent = "No token received."; document.getElementById("output").textContent = "Logged in!"; + document.getElementById("login-section").style.display = "none"; } async function placeOrder() { @@ -31,3 +33,29 @@ async function placeOrder() { const text = await res.text(); document.getElementById("output").textContent = text; } + +async function getOrderHistory() { + const res = await fetch("/api/orders/history", { + method: "GET", + headers: { + "Authorization": "Bearer " + token + } + }); + + if (!res.ok) { + document.getElementById("output").textContent = "Failed to fetch order history."; + return; + } + + const orders = await res.json(); + let output = `Order History for ${orders.username || document.getElementById("username").value}:\n`; + const orderList = orders.orders || orders; + if (orderList.length === 0) { + output += "No orders found.\n"; + } else { + orderList.forEach(order => { + output += `Order #${order.orderId}: ${order.productId} x ${order.quantity}\n`; + }); + } + document.getElementById("output").textContent = output; +} diff --git a/frontend/public/index.html b/frontend/public/index.html index c0fb3c4..31aa74c 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -3,18 +3,129 @@ Demo Shop + -

Login

- - - - -

Order

- - - - -

+  
+
+

Login

+ + + + + +
+
+

Order

+ + + + + +
+
+ +

+    
+
diff --git a/frontend/server.js b/frontend/server.js index 59c0bbb..9ea7642 100644 --- a/frontend/server.js +++ b/frontend/server.js @@ -1,13 +1,55 @@ const express = require("express"); const fetch = require("node-fetch"); const app = express(); +const swaggerUi = require("swagger-ui-express"); +const swaggerJSDoc = require("swagger-jsdoc"); + +const swaggerDefinition = { + openapi: "3.0.0", + info: { + title: "Frontend Gateway API", + version: "1.0.0", + description: "API Gateway for login and order forwarding", + }, + servers: [{ url: "http://localhost:3000" }], +}; + +const swaggerSpec = swaggerJSDoc({ + swaggerDefinition, + apis: ["./server.js"], // this file (can add others too) +}); const AUTH_URL = process.env.AUTH_URL || "http://auth-python:8000"; const ORDERS_URL = process.env.ORDERS_URL || "http://orders-java:8080"; app.use(express.static("public")); +app.get("/swagger.json", (req, res) => { + res.setHeader("Content-Type", "application/json"); + res.send(swaggerSpec); +}); +app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); app.use(express.json()); +/** + * @swagger + * /api/login: + * post: + * summary: Login using auth backend + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * username: + * type: string + * password: + * type: string + * responses: + * 200: + * description: Login successful + */ app.post("/api/login", async (req, res) => { const response = await fetch(`${AUTH_URL}/auth/login`, { method: "POST", @@ -19,6 +61,30 @@ app.post("/api/login", async (req, res) => { res.json(data); }); +/** + * @swagger + * /api/orders: + * post: + * summary: Submit an order to the orders backend + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * example: + * itemId: "abc123" + * quantity: 2 + * responses: + * 200: + * description: Order submitted successfully + * 401: + * description: Unauthorized - invalid or missing token + * 500: + * description: Server error from orders service + */ app.post("/api/orders", async (req, res) => { const token = req.headers["authorization"]; const response = await fetch(`${ORDERS_URL}/orders`, { @@ -34,4 +100,38 @@ app.post("/api/orders", async (req, res) => { res.status(response.status).send(text); }); +/** + * @swagger + * /api/orders/history: + * get: + * summary: Get order history for the authenticated user + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Order history retrieved successfully + * 401: + * description: Unauthorized - invalid or missing token + * 500: + * description: Server error from orders service + */ +app.get("/api/orders/history", async (req, res) => { + const token = req.headers["authorization"]; + + try { + const response = await fetch(`${ORDERS_URL}/orders/history`, { + method: "GET", + headers: { + "Authorization": token, + "Content-Type": "application/json", + }, + }); + + const text = await response.text(); + res.status(response.status).send(text); + } catch (err) { + res.status(500).json({ error: "Failed to fetch order history" }); + } +}); + app.listen(3000, () => console.log("Frontend on :3000")); diff --git a/services/auth-python/app/auth.py b/services/auth-python/app/auth.py index 8b33b52..6d4559b 100644 --- a/services/auth-python/app/auth.py +++ b/services/auth-python/app/auth.py @@ -6,8 +6,7 @@ USER_DB = { "alice": "password123", - "bob": "hunter2", - "admin": "admin" + "bob": "hunter2" } SESSIONS = {} diff --git a/services/billing-csharp/Controllers/BillingController.cs b/services/billing-csharp/Controllers/BillingController.cs index 3f41ab0..28bf688 100644 --- a/services/billing-csharp/Controllers/BillingController.cs +++ b/services/billing-csharp/Controllers/BillingController.cs @@ -3,6 +3,8 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; using System; +using System.Collections.Generic; +using System.IO; public class ChargeRequest { @@ -14,6 +16,10 @@ public class ChargeRequest [JsonPropertyName("quantity")] public int Quantity { get; set; } + + // ISO-8601 timestamp of when the charge was requested + [JsonPropertyName("date")] + public DateTime Date { get; set; } } [ApiController] @@ -21,6 +27,7 @@ public class ChargeRequest public class BillingController : ControllerBase { private readonly string EXPECTED_SECRET = Environment.GetEnvironmentVariable("BILLING_SECRET"); + private static readonly string StorageDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "BillingData"); [HttpPost("charge")] public async Task Charge([FromBody] ChargeRequest request) @@ -37,9 +44,31 @@ public async Task Charge([FromBody] ChargeRequest request) status = "charged", user = request.Username, product = request.ProductId, - quantity = request.Quantity + quantity = request.Quantity, + date = request.Date.ToString("o") // ISO-8601 format }; + await QueueForBillingSystemAsync(request.Username, responsePayload); + return Ok(JsonSerializer.Serialize(responsePayload)); } + + private async Task QueueForBillingSystemAsync(string username, object payload) + { + Directory.CreateDirectory(StorageDirectory); + var filePath = Path.Combine(StorageDirectory, $"{username}.json"); + List payloads = new(); + + if (System.IO.File.Exists(filePath)) + { + try + { + payloads = JsonSerializer.Deserialize>(await System.IO.File.ReadAllTextAsync(filePath)) ?? new(); + } + catch { } + } + + payloads.Add(payload); + await System.IO.File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(payloads, new JsonSerializerOptions { WriteIndented = true })); + } } diff --git a/services/orders-java/pom.xml b/services/orders-java/pom.xml index 02b1ce6..c2eaa3f 100644 --- a/services/orders-java/pom.xml +++ b/services/orders-java/pom.xml @@ -25,17 +25,32 @@ json 20231013 + + com.h2database + h2 + 2.2.224 + runtime + org.springdoc springdoc-openapi-starter-webmvc-ui 2.1.0 + + org.springframework.boot + spring-boot-starter-jdbc + org.junit.jupiter junit-jupiter 5.10.0 test + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/services/orders-java/src/main/java/com/example/orders/config/DataSourceConfig.java b/services/orders-java/src/main/java/com/example/orders/config/DataSourceConfig.java new file mode 100644 index 0000000..0cadfe4 --- /dev/null +++ b/services/orders-java/src/main/java/com/example/orders/config/DataSourceConfig.java @@ -0,0 +1,20 @@ +package com.example.orders.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import javax.sql.DataSource; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +@Configuration +public class DataSourceConfig { + + @Bean + public DataSource dataSource() { + DriverManagerDataSource ds = new DriverManagerDataSource(); + ds.setDriverClassName("org.h2.Driver"); + ds.setUrl("jdbc:h2:mem:ordersdb;DB_CLOSE_DELAY=-1"); + ds.setUsername("sa"); + ds.setPassword(""); + return ds; + } +} \ No newline at end of file diff --git a/services/orders-java/src/main/java/com/example/orders/controller/OrderController.java b/services/orders-java/src/main/java/com/example/orders/controller/OrderController.java index 43bc008..5a13e4a 100644 --- a/services/orders-java/src/main/java/com/example/orders/controller/OrderController.java +++ b/services/orders-java/src/main/java/com/example/orders/controller/OrderController.java @@ -1,24 +1,47 @@ package com.example.orders.controller; +import java.time.Instant; import org.springframework.beans.factory.annotation.Value; import com.example.orders.model.OrderRequest; import org.json.JSONObject; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import jakarta.annotation.PostConstruct; @RestController @RequestMapping("/orders") public class OrderController { - private final RestTemplate restTemplate = new RestTemplate(); - @Value("${AUTH_SERVICE_URL:http://auth-python:8000}") private String authServiceUrl; @Value("${BILLING_SERVICE_URL:http://billing-csharp:80}") private String billingServiceUrl; + protected final RestTemplate restTemplate = new RestTemplate(); + + @Autowired + protected javax.sql.DataSource dataSource; + + // Initialize table after Spring dependency injection + @PostConstruct + public void init() { + try (java.sql.Connection conn = dataSource.getConnection(); + java.sql.Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE IF NOT EXISTS orders (" + + "orderId VARCHAR(255), " + + "username VARCHAR(255), " + + "productId VARCHAR(255), " + + "quantity INT, " + + "timestamp VARCHAR(255))"); + } catch (Exception e) { + // Log error, but don't prevent app startup + e.printStackTrace(); + } + } + @PostMapping public ResponseEntity placeOrder(@RequestBody OrderRequest request, @RequestHeader("Authorization") String authHeader) { @@ -37,9 +60,63 @@ public ResponseEntity placeOrder(@RequestBody OrderRequest request, return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED).body("Billing failed"); } + String orderId = java.util.UUID.randomUUID().toString(); + String timestamp = Instant.now().toString(); + try (java.sql.Connection conn = dataSource.getConnection(); + java.sql.PreparedStatement pstmt = conn.prepareStatement( + "INSERT INTO orders (orderId, username, productId, quantity, timestamp) VALUES (?, ?, ?, ?, ?)")) { + pstmt.setString(1, orderId); + pstmt.setString(2, username); + pstmt.setString(3, request.productId); + pstmt.setInt(4, request.quantity); + pstmt.setString(5, timestamp); + pstmt.executeUpdate(); + } catch (Exception e) { + e.printStackTrace(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Order storage failed"); + } + return ResponseEntity.ok("Order placed successfully by " + username); } + @GetMapping("/history") + public ResponseEntity getOrderHistory(@RequestHeader("Authorization") String authHeader) { + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Missing token"); + } + + String token = authHeader.substring(7); + String username = validateToken(token); + if (username == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid session"); + } + + org.json.JSONArray orders = new org.json.JSONArray(); + try (java.sql.Connection conn = dataSource.getConnection(); + java.sql.PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM orders WHERE username = ?")) { + pstmt.setString(1, username); + try (java.sql.ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + org.json.JSONObject order = new org.json.JSONObject(); + order.put("orderId", rs.getString("orderId")); + order.put("productId", rs.getString("productId")); + order.put("quantity", rs.getInt("quantity")); + order.put("timestamp", rs.getString("timestamp")); + orders.put(order); + } + } + } catch (Exception e) { + e.printStackTrace(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to retrieve order history"); + } + + org.json.JSONObject response = new org.json.JSONObject(); + response.put("username", username); + response.put("orders", orders); + + return ResponseEntity.ok(response.toString()); + } + protected String validateToken(String token) { String authUrl = authServiceUrl + "/auth/validate?token=" + token; try { @@ -49,6 +126,7 @@ protected String validateToken(String token) { String body = authResponse.getBody(); return body.contains("username") ? body.split(":")[1].replaceAll("[\"{} ]", "") : null; } catch (Exception e) { + e.printStackTrace(); return null; } } @@ -62,6 +140,7 @@ protected boolean charge(String username, String productId, int quantity) { payload.put("username", username); payload.put("productId", productId); payload.put("quantity", quantity); + payload.put("date", Instant.now().toString()); HttpEntity entity = new HttpEntity<>(payload.toString(), headers); try { @@ -69,6 +148,7 @@ protected boolean charge(String username, String productId, int quantity) { billingServiceUrl + "/billing/charge", entity, String.class); return billingResponse.getStatusCode().is2xxSuccessful(); } catch (Exception e) { + e.printStackTrace(); return false; } } diff --git a/services/orders-java/src/test/java/com/example/orders/OrdersApplicationTests.java b/services/orders-java/src/test/java/com/example/orders/OrdersApplicationTests.java index 78cd7b8..311d6a3 100644 --- a/services/orders-java/src/test/java/com/example/orders/OrdersApplicationTests.java +++ b/services/orders-java/src/test/java/com/example/orders/OrdersApplicationTests.java @@ -18,6 +18,12 @@ public MockedOrderController(boolean validToken, boolean billingSuccess, String this.validToken = validToken; this.billingSuccess = billingSuccess; this.mockUser = mockUser; + org.h2.jdbcx.JdbcDataSource dataSource = new org.h2.jdbcx.JdbcDataSource(); + dataSource.setURL("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"); + dataSource.setUser("sa"); + dataSource.setPassword(""); + this.dataSource = dataSource; + super.init(); } @Override