Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
32 changes: 30 additions & 2 deletions frontend/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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;
}
133 changes: 122 additions & 11 deletions frontend/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,129 @@
<head>
<title>Demo Shop</title>
<script defer src="app.js"></script>
<style>
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: #f4f6fb;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.container {
background: #fff;
margin-top: 40px;
padding: 32px 40px;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
min-width: 320px;
}
h1 {
margin-top: 0;
color: #2d3a4b;
font-size: 1.5rem;
border-bottom: 1px solid #e0e4ea;
padding-bottom: 8px;
margin-bottom: 18px;
}
label {
display: block;
margin-bottom: 6px;
color: #4a5568;
font-size: 0.97rem;
}
input {
width: 100%;
padding: 8px 10px;
margin-bottom: 16px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 1rem;
background: #f8fafc;
transition: border 0.2s;
}
input:focus {
border-color: #4f8cff;
outline: none;
}
button {
background: #4f8cff;
color: #fff;
border: none;
border-radius: 6px;
padding: 10px 18px;
font-size: 1rem;
cursor: pointer;
margin-right: 8px;
margin-bottom: 10px;
transition: background 0.2s;
}
button:hover {
background: #2563eb;
}
#output {
background: #f1f5f9;
border-radius: 6px;
padding: 12px;
margin-top: 18px;
min-height: 40px;
font-size: 0.97rem;
color: #374151;
border: 1px solid #e2e8f0;
white-space: pre-wrap;
}
.section {
margin-bottom: 28px;
}
select {
width: 100%;
padding: 8px 10px;
margin-bottom: 16px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 1rem;
background: #f8fafc;
transition: border 0.2s;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}

select:focus {
border-color: #4f8cff;
outline: none;
}
</style>
</head>
<body>
<h1>Login</h1>
<input id="username" placeholder="Username" />
<input id="password" type="password" />
<button onclick="login()">Login</button>

<h1>Order</h1>
<input id="product" placeholder="Product ID" />
<input id="quantity" type="number" value="1" />
<button onclick="placeOrder()">Place Order</button>

<pre id="output"></pre>
<div class="container">
<div class="section" id="login-section">
<h1>Login</h1>
<label for="username">Username</label>
<input id="username" placeholder="Username" />
<label for="password">Password</label>
<input id="password" type="password" placeholder="Password" />
<button onclick="login()">Login</button>
</div>
<div class="section">
<h1>Order</h1>
<label for="product">Product ID</label>
<select id="product" style="width: 100%; padding: 8px 10px; margin-bottom: 16px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 1rem; background: #f8fafc; transition: border 0.2s;">
<option value="" disabled selected>Select a product</option>
<option value="101">Wireless Headphones (101)</option>
<option value="102">Smart Fitness Watch (102)</option>
<option value="103">Bluetooth Speaker (103)</option>
</select>
<label for="quantity">Quantity</label>
<input id="quantity" type="number" value="1" min="1" />
<button onclick="placeOrder()">Place Order</button>
</div>
<div class="section">
<button onclick="getOrderHistory()">Order History</button>
<pre id="output"></pre>
</div>
</div>
</body>
</html>
100 changes: 100 additions & 0 deletions frontend/server.js
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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`, {
Expand All @@ -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"));
3 changes: 1 addition & 2 deletions services/auth-python/app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@

USER_DB = {
"alice": "password123",
"bob": "hunter2",
"admin": "admin"
"bob": "hunter2"
}

SESSIONS = {}
Expand Down
31 changes: 30 additions & 1 deletion services/billing-csharp/Controllers/BillingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -14,13 +16,18 @@ 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]
[Route("billing")]
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<IActionResult> Charge([FromBody] ChargeRequest request)
Expand All @@ -37,9 +44,31 @@ public async Task<IActionResult> 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<object> payloads = new();

if (System.IO.File.Exists(filePath))
{
try
{
payloads = JsonSerializer.Deserialize<List<object>>(await System.IO.File.ReadAllTextAsync(filePath)) ?? new();
}
catch { }
}

payloads.Add(payload);
await System.IO.File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(payloads, new JsonSerializerOptions { WriteIndented = true }));
}
}
Loading