commit ae1a16f94c05831f720e17af87ffde6d8e96bed2 Author: Steve Waterworth Date: Wed Jan 10 16:31:49 2018 +0000 initial load diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07eb7e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.*.swp +.DS_Store + diff --git a/cart/Dockerfile b/cart/Dockerfile new file mode 100644 index 0000000..4fe6fa2 --- /dev/null +++ b/cart/Dockerfile @@ -0,0 +1,14 @@ +FROM node:8 + +EXPOSE 8080 + +WORKDIR /opt/server + +COPY package.json /opt/server/ + +RUN npm install + +COPY server.js /opt/server/ + +CMD ["node", "server.js"] + diff --git a/cart/package.json b/cart/package.json new file mode 100644 index 0000000..73cd064 --- /dev/null +++ b/cart/package.json @@ -0,0 +1,18 @@ +{ + "name": "catalogue", + "version": "1.0.0", + "description": "product catalogue REST API", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "SteveW", + "license": "Apache-2.0", + "dependencies": { + "body-parser": "^1.18.1", + "express": "^4.15.4", + "redis": "^2.8.0", + "request": "^2.83.0", + "instana-nodejs-sensor": "^1.28.0" + } +} diff --git a/cart/server.js b/cart/server.js new file mode 100644 index 0000000..a556861 --- /dev/null +++ b/cart/server.js @@ -0,0 +1,234 @@ +const instana = require('instana-nodejs-sensor'); +const redis = require('redis'); +const request = require('request'); +const bodyParser = require('body-parser'); +const express = require('express'); + +// init tracing +instana({ + tracing: { + enabled: true + } +}); + +var redisConnected = false; + +const app = express(); + +app.use((req, res, next) => { + res.set('Timing-Allow-Origin', '*'); + res.set('Access-Control-Allow-Origin', '*'); + next(); +}); + +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); + +app.get('/health', (req, res) => { + var stat = { + app: 'OK', + redis: redisConnected + }; + res.json(stat); +}); + + +// get cart with id +app.get('/cart/:id', (req, res) => { + redisClient.get(req.params.id, (err, data) => { + if(err) { + console.log('ERROR', err); + res.status(500).send(err); + } else { + if(data == null) { + res.status(404).send('cart not found'); + } else { + res.set('Content-Type', 'application/json'); + res.send(data); + } + } + }); +}); + +// update/create cart +app.get('/add/:id/:sku/:qty', (req, res) => { + // check quantity + var qty = parseInt(req.params.qty); + if(isNaN(qty)) { + res.status(400).send('quantity must be a number'); + return; + } else if(qty < 1) { + res.status(400).send('negative quantity not allowed'); + return; + } + + // look up product details + getProduct(req.params.sku).then((product) => { + console.log('got product', product); + // is the product in stock? + if(product.instock == 0) { + res.status(404).send('out of stock'); + return; + } + // does the cart already exist? + redisClient.get(req.params.id, (err, data) => { + if(err) { + console.log('ERROR', err); + res.status(500).send(err); + } else { + var cart; + if(data == null) { + // create new cart + cart = { + total: 0, + tax: 0, + items: [] + }; + } else { + cart = JSON.parse(data); + } + console.log('got cart', cart); + // add sku to cart + var item = { + qty: qty, + sku: req.params.sku, + name: product.name, + price: product.price, + subtotal: qty * product.price + }; + var list = mergeList(cart.items, item, qty); + cart.items = list; + cart.total = calcTotal(cart.items); + // work out tax @ 20% + cart.tax = (cart.total - (cart.total / 1.2)).toFixed(2); + + // save the new cart + saveCart(req.params.id, cart); + res.json(cart); + } + }); + }).catch((err) => { + res.status(500).send(err); + }); +}); + +// update quantity - remove item when qty == 0 +app.get('/update/:id/:sku/:qty', (req, res) => { + // check quantity + var qty = parseInt(req.params.qty); + if(isNaN(qty)) { + res.status(400).send('quantity must be a number'); + return; + } + + // get the cart + redisClient.get(req.params.id, (err, data) => { + if(err) { + console.log('ERROR', err); + res.status(500).send(err); + } else { + if(data == null) { + res.status(404).send('cart not found'); + } else { + var cart = JSON.parse(data); + var idx; + var len = cart.items.length; + for(idx = 0; idx < len; idx++) { + if(cart.items[idx].sku == req.params.sku) { + break; + } + } + if(idx == len) { + // not in list + res.status(404).send('not in cart'); + } else { + if(qty == 0) { + cart.items.splice(idx, 1); + } else { + cart.items[idx].qty = qty; + cart.items[idx].subtotal = cart.items[idx].price * qty; + } + cart.total = calcTotal(cart.items); + // work out tax @ 20% + cart.tax = (cart.total - (cart.total / 1.2)).toFixed(2); + saveCart(req.params.id, cart); + res.json(cart); + } + } + } + }); +}); + +function mergeList(list, product, qty) { + var inlist = false; + // loop through looking for sku + var idx; + var len = list.length; + for(idx = 0; idx < len; idx++) { + if(list[idx].sku == product.sku) { + inlist = true; + break; + } + } + + if(inlist) { + list[idx].qty += qty; + list[idx].subtotal = list[idx].price * list[idx].qty; + } else { + list.push(product); + } + + return list; +} + +function calcTotal(list) { + var total = 0; + for(var idx = 0, len = list.length; idx < len; idx++) { + total += list[idx].subtotal; + } + + return total; +} + +function getProduct(sku) { + return new Promise((resolve, reject) => { + request('http://catalogue:8080/product/' + sku, (err, res, body) => { + if(err) { + reject(err); + } else if(res.statusCode != 200) { + reject(res.statusCode); + } else { + // return object - body is a string + resolve(JSON.parse(body)); + } + }); + }); +} + +function saveCart(id, cart) { + console.log('saving cart', cart); + redisClient.setex(id, 3600, JSON.stringify(cart), (err, data) => { + if(err) { + console.log('saveCart ERROR', err); + } + }); +} + +// connect to Redis +var redisClient = redis.createClient({ + host: 'redis' +}); + +redisClient.on('error', (e) => { + console.log('Redis ERROR', e); +}); +redisClient.on('ready', (r) => { + console.log('Redis READY', r); + redisConnected = true; +}); + +// fire it up! +const port = process.env.CART_SERVER_PORT || '8080'; +app.listen(port, () => { + console.log('Started on port', port); +}); diff --git a/catalogue/Dockerfile b/catalogue/Dockerfile new file mode 100644 index 0000000..4fe6fa2 --- /dev/null +++ b/catalogue/Dockerfile @@ -0,0 +1,14 @@ +FROM node:8 + +EXPOSE 8080 + +WORKDIR /opt/server + +COPY package.json /opt/server/ + +RUN npm install + +COPY server.js /opt/server/ + +CMD ["node", "server.js"] + diff --git a/catalogue/package.json b/catalogue/package.json new file mode 100644 index 0000000..ef637e8 --- /dev/null +++ b/catalogue/package.json @@ -0,0 +1,17 @@ +{ + "name": "catalogue", + "version": "1.0.0", + "description": "product catalogue REST API", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "SteveW", + "license": "Apache-2.0", + "dependencies": { + "body-parser": "^1.18.1", + "express": "^4.15.4", + "mongodb": "^2.2.33", + "instana-nodejs-sensor": "^1.28.0" + } +} diff --git a/catalogue/server.js b/catalogue/server.js new file mode 100644 index 0000000..b51dd53 --- /dev/null +++ b/catalogue/server.js @@ -0,0 +1,146 @@ +const instana = require('instana-nodejs-sensor'); +const mongoClient = require('mongodb').MongoClient; +const mongoObjectID = require('mongodb').ObjectID; +const bodyParser = require('body-parser'); +const express = require('express'); + +// MongoDB +var db; +var collection; +var mongoConnected = false; + +// init tracing +instana({ + tracing: { + enabled: true + } +}); + + +const app = express(); + +app.use((req, res, next) => { + res.set('Timing-Allow-Origin', '*'); + res.set('Access-Control-Allow-Origin', '*'); + next(); +}); + +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); + +app.get('/health', (req, res) => { + var stat = { + app: 'OK', + mongo: mongoConnected + }; + res.json(stat); +}); + +// all products +app.get('/products', (req, res) => { + if(mongoConnected) { + collection.find({}).toArray().then((products) => { + res.json(products); + }).catch((e) => { + console.log('ERROR', e); + res.status(500).send(e); + }); + } else { + res.status(500).send('database not avaiable'); + } +}); + +// product by SKU +app.get('/product/:sku', (req, res) => { + if(mongoConnected) { + collection.findOne({sku: req.params.sku}).then((product) => { + console.log('product', product); + if(product) { + res.json(product); + } else { + res.status(404).send('SKU not found'); + } + }).catch((e) => { + console.log('ERROR', e); + res.status(500).send(e); + }); + } else { + res.status(500).send('database not available'); + } +}); + +// products in a category +app.get('/products/:cat', (req, res) => { + if(mongoConnected) { + collection.find({ categories: req.params.cat}).toArray().then((products) => { + res.json(products); + }).catch((e) => { + console.log('ERROR', e); + res.status(500).send(e); + }); + } else { + res.status(500).send('database not avaiable'); + } +}); + +// all categories +app.get('/categories', (req, res) => { + if(mongoConnected) { + collection.distinct('categories').then((categories) => { + res.json(categories); + }).catch((e) => { + console.log('ERROR', e); + res.status(500).send(e); + }); + } else { + res.status(500).send('database not available'); + } +}); + +// search name and description +app.get('/search/:text', (req, res) => { + if(mongoConnected) { + collection.find({ '$text': { '$search': req.params.text }}).toArray().then((hits) => { + res.json(hits); + }).catch((e) => { + console.log('ERROR', e); + res.status(500).send(e); + }); + } else { + res.status(500).send('database not available'); + } +}); + +// set up Mongo +function mongoConnect() { + return new Promise((resolve, reject) => { + var mongoURL = process.env.MONGO_URL || 'mongodb://mongodb:27017/catalogue'; + mongoClient.connect(mongoURL, (error, _db) => { + if(error) { + reject(error); + } else { + db = _db; + collection = db.collection('products'); + resolve('connected'); + } + }); +}); +} + +function mongoLoop() { + mongoConnect().then((r) => { + mongoConnected = true; + console.log('MongoDB connected'); + }).catch((e) => { + console.error('ERROR', e); + setTimeout(mongoLoop, 2000); + }); +} + +mongoLoop(); + +// fire it up! +const port = process.env.CATALOGUE_SERVER_PORT || '8080'; +app.listen(port, () => { + console.log('Started on port', port); +}); diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..b6e91aa --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,70 @@ +version: '3' +services: + mongodb: + build: + context: mongo + image: steveww/rs-mongodb + ports: + - "27017" + networks: + - robot-shop + redis: + image: redis:4.0.5 + ports: + - "6379" + networks: + - robot-shop + catalogue: + build: + context: catalogue + image: steveww/rs-catalogue + depends_on: + - mongodb + ports: + - "8080" + networks: + - robot-shop + user: + build: + context: user + image: steveww/rs-user + depends_on: + - mongodb + - redis + ports: + - "8080" + networks: + - robot-shop + cart: + build: + context: cart + image: steveww/rs-cart + depends_on: + - redis + ports: + - "8080" + networks: + - robot-shop + shipping: + build: + context: shipping + image: steveww/rs-shipping + ports: + - "8080" + networks: + - robot-shop + web: + build: + context: web + image: steveww/rs-web + depends_on: + - catalogue + - user + ports: + - "8080:8080" + networks: + - robot-shop + +networks: + robot-shop: + diff --git a/instana/instana-agent-private.yml b/instana/instana-agent-private.yml new file mode 100644 index 0000000..34a778d --- /dev/null +++ b/instana/instana-agent-private.yml @@ -0,0 +1,112 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: instana-agent + labels: + name: instana-agent + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: instana-admin + namespace: instana-agent + +--- +apiVersion: v1 +kind: Secret +metadata: + name: instana-agent-secret + namespace: instana-agent +data: + key: SVlVS0tpbUNRLTZxVlRhd3FDSFBFdw== + +--- +apiVersion: extensions/v1beta1 +kind: DaemonSet +metadata: + labels: + app: instana-agent + name: instana-agent + namespace: instana-agent +spec: + selector: + matchLabels: + app: instana-agent + template: + metadata: + labels: + app: instana-agent + spec: + nodeSelector: + agent: instana + hostIPC: true + hostNetwork: true + hostPID: true + containers: + - name: instana-agent + image: instana/agent + imagePullPolicy: Always + env: + - name: INSTANA_ZONE + value: Robot-Shop + - name: INSTANA_AGENT_ENDPOINT + value: saas-us-west-2.instana.io + - name: INSTANA_AGENT_ENDPOINT_PORT + value: '443' + - name: INSTANA_AGENT_KEY + valueFrom: + secretKeyRef: + name: instana-agent-secret + key: key + - name: INSTANA_AGENT_HTTP_LISTEN + value: '*' + securityContext: + privileged: true + volumeMounts: + - name: dev + mountPath: /dev + - name: run + mountPath: /var/run/docker.sock + - name: sys + mountPath: /sys + - name: log + mountPath: /var/log + livenessProbe: + httpGet: + path: / + port: 42699 + initialDelaySeconds: 120 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 5 + readinessProbe: + httpGet: + path: / + port: 42699 + initialDelaySeconds: 120 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 5 + resources: + requests: + memory: "256Mi" + cpu: "0.2" + limits: + memory: "512Mi" + cpu: "0.5" + volumes: + - name: dev + hostPath: + path: /dev + - name: run + hostPath: + path: /var/run/docker.sock + - name: sys + hostPath: + path: /sys + - name: log + hostPath: + path: /var/log + diff --git a/mongo/Dockerfile b/mongo/Dockerfile new file mode 100644 index 0000000..65c7413 --- /dev/null +++ b/mongo/Dockerfile @@ -0,0 +1,4 @@ +FROM mongo:3.6.1 + +COPY *.js /docker-entrypoint-initdb.d/ + diff --git a/mongo/catalogue.js b/mongo/catalogue.js new file mode 100644 index 0000000..37b3d0d --- /dev/null +++ b/mongo/catalogue.js @@ -0,0 +1,23 @@ +// +// Products +// +db = db.getSiblingDB('catalogue'); +db.products.insertMany([ + {sku: 'PB-1', name: 'Positronic Brain', description: 'Highly advanced sentient processing unit', price: 42000, instock: 0, categories: ['components']}, + {sku: 'SVO-980', name: 'Servo 980Nm', description: 'Servo actuator with 980Nm of torque. Needs 24V 10A supply', price: 50, instock: 32, categories: ['components']}, + {sku: 'ROB-1', name: 'Robbie', description: 'Large mechanical workhorse, crude but effective', price: 1200, instock: 12, categories: ['complete']}, + {sku: 'EVE-1', name: 'Eve', description: 'Extraterrestrial Vegetation Evaluator', price: 5000, instock: 10, categories: ['complete']} +]); + +// full text index for searching +db.products.createIndex({ + name: "text", + description: "text" +}); + +// unique index for product sku +db.products.createIndex( + { sku: 1 }, + { unique: true } +); + diff --git a/mongo/mongo-dep.yaml b/mongo/mongo-dep.yaml new file mode 100644 index 0000000..e296d41 --- /dev/null +++ b/mongo/mongo-dep.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: mongodb + namespace: robot-shop + labels: + app: mongodb +spec: + replicas: 1 + selector: + matchLabels: + app: mongodb + template: + metadata: + labels: + app: mongodb + spec: + containers: + - name: mongodb + image: steveww/rs-mongodb + ports: + - containerPort: 27017 diff --git a/mongo/mongo-svc.yaml b/mongo/mongo-svc.yaml new file mode 100644 index 0000000..b78d3b1 --- /dev/null +++ b/mongo/mongo-svc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongodb + namespace: robot-shop + labels: + app: mongodb +spec: + ports: + - name: mongodb + port: 27017 + targetPort: 27017 + selector: + app: mongodb diff --git a/mongo/users.js b/mongo/users.js new file mode 100644 index 0000000..3b52e46 --- /dev/null +++ b/mongo/users.js @@ -0,0 +1,15 @@ +// +// Products +// +db = db.getSiblingDB('users'); +db.users.insertMany([ + {name: 'user', password: 'password', email: 'user@me.com'}, + {name: 'stan', password: 'bigbrain', email: 'stan@instana.com'} +]); + +// unique index on the name +db.users.createIndex( + {name: 1}, + {unique: true} +); + diff --git a/shipping/Dockerfile b/shipping/Dockerfile new file mode 100644 index 0000000..6bd8156 --- /dev/null +++ b/shipping/Dockerfile @@ -0,0 +1,28 @@ +# +# Build +# +FROM openjdk:8-jdk as build + +RUN apt-get update && apt-get -y install maven + +WORKDIR /opt/shipping + +COPY pom.xml /opt/shipping/ +RUN mvn install + +COPY src /opt/shipping/src/ +RUN mvn package + +# +# Run +# +FROM openjdk:8-jdk + +EXPOSE 8080 + +WORKDIR /opt/shipping + +COPY --from=build /opt/shipping/target/shipping-1.0-jar-with-dependencies.jar shipping.jar + +CMD [ "java", "-jar", "shipping.jar" ] + diff --git a/shipping/pom.xml b/shipping/pom.xml new file mode 100644 index 0000000..eb04318 --- /dev/null +++ b/shipping/pom.xml @@ -0,0 +1,58 @@ + + 4.0.0 + steveww + shipping + 1.0 + jar + Spark Java Sample + + + 1.8 + 1.8 + + + + + com.sparkjava + spark-core + 2.7.1 + + + org.slf4j + slf4j-simple + 1.7.25 + + + org.eclipse.jetty + jetty-jmx + 9.4.6.v20170531 + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + package + + single + + + + + org.steveww.spark.Main + + + + jar-with-dependencies + + + + + + + + diff --git a/shipping/shipping-dep.yaml b/shipping/shipping-dep.yaml new file mode 100644 index 0000000..3fba06b --- /dev/null +++ b/shipping/shipping-dep.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: shipping + namespace: robot-shop + labels: + app: shipping +spec: + replicas: 1 + selector: + matchLabels: + app: shipping + template: + metadata: + labels: + app: shipping + spec: + containers: + - name: shipping + image: steveww/rs-shipping + ports: + - containerPort: 8080 diff --git a/shipping/shipping-svc.yaml b/shipping/shipping-svc.yaml new file mode 100644 index 0000000..828f9f8 --- /dev/null +++ b/shipping/shipping-svc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: shipping + namespace: robot-shop + labels: + app: shipping +spec: + ports: + - name: shipping + port: 8080 + targetPort: 8080 + selector: + app: shipping diff --git a/shipping/shipping.jar b/shipping/shipping.jar new file mode 100644 index 0000000..661a5e5 Binary files /dev/null and b/shipping/shipping.jar differ diff --git a/shipping/src/main/java/org/steveww/spark/Main.java b/shipping/src/main/java/org/steveww/spark/Main.java new file mode 100644 index 0000000..c0f7989 --- /dev/null +++ b/shipping/src/main/java/org/steveww/spark/Main.java @@ -0,0 +1,17 @@ +package org.steveww.spark; + +import spark.Spark; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Main { + private static Logger logger = LoggerFactory.getLogger(Main.class); + + public static void main(String[] args) { + Spark.port(8080); + Spark.get("/hello", (req, res) -> "Hello World"); + Spark.awaitInitialization(); + logger.info("Ready"); + } +} diff --git a/user/Dockerfile b/user/Dockerfile new file mode 100644 index 0000000..4fe6fa2 --- /dev/null +++ b/user/Dockerfile @@ -0,0 +1,14 @@ +FROM node:8 + +EXPOSE 8080 + +WORKDIR /opt/server + +COPY package.json /opt/server/ + +RUN npm install + +COPY server.js /opt/server/ + +CMD ["node", "server.js"] + diff --git a/user/package.json b/user/package.json new file mode 100644 index 0000000..04078f0 --- /dev/null +++ b/user/package.json @@ -0,0 +1,18 @@ +{ + "name": "catalogue", + "version": "1.0.0", + "description": "product catalogue REST API", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "SteveW", + "license": "Apache-2.0", + "dependencies": { + "body-parser": "^1.18.1", + "express": "^4.15.4", + "mongodb": "^2.2.33", + "redis": "^2.8.0", + "instana-nodejs-sensor": "^1.28.0" + } +} diff --git a/user/server.js b/user/server.js new file mode 100644 index 0000000..d50c5f1 --- /dev/null +++ b/user/server.js @@ -0,0 +1,170 @@ +const instana = require('instana-nodejs-sensor'); +const mongoClient = require('mongodb').MongoClient; +const mongoObjectID = require('mongodb').ObjectID; +const redis = require('redis'); +const bodyParser = require('body-parser'); +const express = require('express'); + +// MongoDB +var db; +var collection; +var mongoConnected = false; + +// init tracing +instana({ + tracing: { + enabled: true + } +}); + + +const app = express(); + +app.use((req, res, next) => { + res.set('Timing-Allow-Origin', '*'); + res.set('Access-Control-Allow-Origin', '*'); + next(); +}); + +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); + +app.get('/health', (req, res) => { + var stat = { + app: 'OK', + mongo: mongoConnected + }; + res.json(stat); +}); + +// use REDIS INCR to track anonymous users +app.get('/uniqueid', (req, res) => { + // get number from Redis + redisClient.incr('user', (err, r) => { + if(!err) { + res.json({ + uuid: 'anonymous-' + r + }); + } + }); +}); + +// return all users for debugging only +app.get('/users', (req, res) => { + if(mongoConnected) { + collection.find().toArray().then((users) => { + res.json(users); + }).catch((e) => { + console.log('ERROR', e); + res.status(500).send(e); + }); + } else { + res.status(500).send('database not available'); + } +}); + +app.post('/login', (req, res) => { + console.log('login', req.body); + if(req.body.name === undefined || req.body.password === undefined) { + res.status(400).send('name or passowrd not supplied'); + } else if(mongoConnected) { + collection.findOne({ + name: req.body.name, + }).then((user) => { + console.log('user', user); + if(user) { + if(user.password == req.body.password) { + res.json(user); + } else { + res.status(404).send('incorrect password'); + } + } else { + res.status(404).send('name not found'); + } + }).catch((e) => { + console.log('ERROR', e); + res.status(500).send(e); + }); + } else { + res.status(500).send('database not available'); + } +}); + +// TODO - validate email address format +app.post('/register', (req, res) => { + console.log('register', req.body); + if(req.body.name === undefined || req.body.password === undefined || req.body.email === undefined) { + res.status(400).send('insufficient data'); + } else if(mongoConnected) { + // check if name already exists + collection.findOne({name: req.body.name}).then((user) => { + if(user) { + res.status(400).send('name already exists'); + } else { + // create new user + collection.insertOne({ + name: req.body.name, + password: req.body.password, + email: req.body.email + }).then((r) => { + console.log('inserted', r.result); + res.send('OK'); + }).catch((e) => { + console.log('ERROR', e); + res.status(500).send(e); + }); + } + }).catch((e) => { + console.log('ERROR', e); + res.status(500).send(e); + }); + } else { + res.status(500).send('database not available'); + } +}); + +// connect to Redis +var redisClient = redis.createClient({ + host: 'redis' +}); + +redisClient.on('error', (e) => { + console.log('Redis ERROR', e); +}); +redisClient.on('ready', (r) => { + console.log('Redis READY', r); +}); + +// set up Mongo +function mongoConnect() { + return new Promise((resolve, reject) => { + var mongoURL = process.env.MONGO_URL || 'mongodb://mongodb:27017/users'; + mongoClient.connect(mongoURL, (error, _db) => { + if(error) { + reject(error); + } else { + db = _db; + collection = db.collection('users'); + resolve('connected'); + } + }); +}); +} + +function mongoLoop() { + mongoConnect().then((r) => { + mongoConnected = true; + console.log('MongoDB connected'); + }).catch((e) => { + console.error('ERROR', e); + setTimeout(mongoLoop, 2000); + }); +} + +mongoLoop(); + +// fire it up! +const port = process.env.USER_SERVER_PORT || '8080'; +app.listen(port, () => { + console.log('Started on port', port); +}); diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..0e21260 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,10 @@ +FROM nginx:1.13.8 + +EXPOSE 8080 + +# RUN apt-get update -y && apt-get install -y curl iputils-ping dnsutils +# RUN mkdir -p /var/cache/nginx && chmod 777 /var/cache/nginx + +COPY default.conf /etc/nginx/conf.d/default.conf +COPY static /usr/share/nginx/html + diff --git a/web/default.conf b/web/default.conf new file mode 100644 index 0000000..3bc5519 --- /dev/null +++ b/web/default.conf @@ -0,0 +1,62 @@ +server { + listen 8080; + server_name localhost; + + #charset koi8-r; + #access_log /var/log/nginx/host.access.log main; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + # proxy the PHP scripts to Apache listening on 127.0.0.1:80 + # + #location ~ \.php$ { + # proxy_pass http://127.0.0.1; + #} + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + #location ~ \.php$ { + # root html; + # fastcgi_pass 127.0.0.1:9000; + # fastcgi_index index.php; + # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; + # include fastcgi_params; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} + + location /api/catalogue/ { + proxy_pass http://catalogue:8080/; + } + + location /api/user/ { + proxy_pass http://user:8080/; + } + + location /api/cart/ { + proxy_pass http://cart:8080/; + } + + location /nginx_status { + stub_status on; + access_log off; + } +} + diff --git a/web/static/cart.html b/web/static/cart.html new file mode 100644 index 0000000..01da220 --- /dev/null +++ b/web/static/cart.html @@ -0,0 +1,4 @@ + +
+ Shopping cart for {{ data.uniqueid }} will be here +
diff --git a/web/static/css/style.css b/web/static/css/style.css new file mode 100644 index 0000000..901a96e --- /dev/null +++ b/web/static/css/style.css @@ -0,0 +1,146 @@ +:root { + font-family: sans-serif; +} + +body { + background: linear-gradient(180deg, #0c1415, #16363e); + color: cyan; + height: 1000px; +} + +h1 { + font-family: 'Orbitron', sans-serif; +} + +h1 a:link { + color: cyan; + text-decoration: none; +} + +h1 a:visited { + color: cyan; + text-decoration: none; +} + +h3 { + font-family: 'Orbitron', sans-serif; + margin: 5px; +} + +h3 a:link { + color: cyan; + text-decoration: none; +} + +h3 a:visited { + color: cyan; + text-decoration: none; +} + +.category { + font-size: 14pt; + cursor: context-menu; + text-transform: capitalize; +} + +ul.products { + margin: 5px; +} + +a.product:visited { + color: cyan; + text-decoration: none; +} + +a.product:link { + color: cyan; + text-decoration: none; +} + +a.product:hover { + color: white; + text-decoration: none; +} + +#stan { + float: right; + position: relative; + top: -75px; + padding: 0px; + margin-top: 0px; + margin-right: 20%; + margin-bottom: 0px; + margin-left: 0px; +} + +#search { + float: right; + position: relative; + top: -50px; +} + +.row, .column, .nav, .main { + margin: 0px; + box-sizing: border-box; +} + +.column { + float: left; + padding: 10px; +} + +.row:after { + content: ""; + display: table; + clear: both; +} + +.nav { + width: 25%; +} + +.main { + width: auto; +} + +.footer { + font-size: 8pt; +} + +/* product details */ +.productimage { + border: 2px solid cyan; + padding: 5px; + margin: 10px; + width: 300px; + height: 300px; +} + +.description { + width: 300px; + margin: 10px; +} + +.productcart { + border: 1px solid cyan; + margin: 10px; + padding: 5px; + width: 300px; +} + +/* login register */ +table.credentials { + border: 2px solid cyan; + padding: 10px; + margin: 10px; +} + +.message { + color: magenta; + font-weight: bold; +} + +.centre { + text-align: center; +} + diff --git a/web/static/index.html b/web/static/index.html new file mode 100644 index 0000000..3fd460f --- /dev/null +++ b/web/static/index.html @@ -0,0 +1,65 @@ + + + + Stan's Robot Shop + + + + + + +
+

Stan's Robot Shop

+ + + + +
+ + +
+
+ +
+ + + + +
+
+
+
+ + + +
+ + + + + + + + + diff --git a/web/static/js/controller.js b/web/static/js/controller.js new file mode 100644 index 0000000..2f5442f --- /dev/null +++ b/web/static/js/controller.js @@ -0,0 +1,234 @@ +(function(angular) { + 'use strict'; + + var robotshop = angular.module('robotshop', ['ngRoute']) + + // Share user between controllers + robotshop.factory('currentUser', function() { + var data = { + uniqueid: '', + user: {}, + }; + + return data; + }); + + robotshop.config(['$routeProvider', '$locationProvider', ($routeProvider, $locationProvider) => { + $routeProvider.when('/', { + templateUrl: 'splash.html', + controller: 'shopform' + }).when('/product/:sku', { + templateUrl: 'product.html', + controller: 'productform' + }).when('/login', { + templateUrl: 'login.html', + controller: 'loginform' + }).when('/cart', { + templateUrl: 'cart.html', + controller: 'cartform' + }); + + // needed for URL rewrite hash + $locationProvider.html5Mode(true); + }]); + + // clear template fragment cache + robotshop.run(function($rootScope, $templateCache) { + $rootScope.$on('$viewContentLoaded', function() { + console.log('>>> clearing cache'); + $templateCache.removeAll(); + }); + }); + + robotshop.controller('shopform', function($scope, $http, currentUser) { + $scope.data = {}; + + $scope.data.uniqueid = 'foo'; + $scope.data.categories = []; + $scope.data.products = {}; + + $scope.getProducts = function(category) { + if($scope.data.products[category]) { + $scope.data.products[category] = null; + } else { + $http({ + url: '/api/catalogue/products/' + category, + method: 'GET' + }).then((res) => { + $scope.data.products[category] = res.data; + }).catch((e) => { + console.log('ERROR', e); + }); + } + }; + + function getCategories() { + $http({ + url: '/api/catalogue/categories', + method: 'GET' + }).then((res) => { + $scope.data.categories = res.data; + console.log('categories loaded'); + }).catch((e) => { + console.log('ERROR', e); + }); + } + + // unique id for cart etc + function getUniqueid() { + return new Promise((resolve, reject) => { + $http({ + url: '/api/user/uniqueid', + method: 'GET' + }).then((res) => { + resolve(res.data.uuid); + }).catch((e) => { + console.log('ERROR', e); + reject(e); + }); + }); + } + + // init + console.log('shopform starting...'); + getCategories(); + if(!currentUser.uniqueid) { + console.log('generating uniqueid'); + getUniqueid().then((id) => { + $scope.data.uniqueid = id; + currentUser.uniqueid = id; + }).catch((e) => { + console.log('ERROR', e); + }); + } + + // watch for login + $scope.$watch(() => { return currentUser.uniqueid; }, (newVal, oldVal) => { + if(newVal !== oldVal) { + $scope.data.uniqueid = currentUser.uniqueid; + } + }); + }); + + robotshop.controller('productform', function($scope, $http, $routeParams, currentUser) { + $scope.data = {}; + $scope.data.message = ''; + $scope.data.product = {}; + $scope.data.quantity = 1; + + $scope.addToCart = function() { + var url = '/api/cart/add/' + currentUser.uniqueid + '/' + $scope.data.product.sku + '/' + $scope.data.quantity; + console.log('addToCart', url); + $http({ + url: url, + method: 'GET' + }).then((res) => { + console.log('cart', res); + $scope.data.message = 'Added to cart'; + setTimeout(clearMessage, 3000); + }).catch((e) => { + console.log('ERROR', e); + $scope.data.message = 'ERROR ' + e; + setTimeout(clearMessage, 3000); + }); + }; + + function loadProduct(sku) { + $http({ + url: '/api/catalogue/product/' + sku, + method: 'GET' + }).then((res) => { + $scope.data.product = res.data; + }).catch((e) => { + console.log('ERROR', e); + }); + } + + function clearMessage() { + $scope.data.message = ''; + } + + loadProduct($routeParams.sku); + }); + + robotshop.controller('cartform', function($scope, $http, currentUser) { + $scope.data = {}; + $scope.data.uniqueid = currentUser.uniqueid; + }); + + robotshop.controller('loginform', function($scope, $http, currentUser) { + $scope.data = {}; + $scope.data.name = ''; + $scope.data.email = ''; + $scope.data.password = ''; + $scope.data.password2 = ''; + $scope.data.message = ''; + $scope.data.user = {}; + + $scope.login = function() { + $scope.data.message = ''; + $http({ + url: '/api/user/login', + method: 'POST', + data: { + name: $scope.data.name, + password: $scope.data.password, + email: $scope.data.email + } + }).then((res) => { + $scope.data.user = res.data; + $scope.data.user.password = ''; + $scope.data.password = $scope.data.password2 = ''; + currentUser.user = $scope.data.user; + currentUser.uniqueid = $scope.data.user.name; + }).catch((e) => { + console.log('ERROR', e); + $scope.data.message = 'ERROR ' + e.data; + $scope.data.password = ''; + }); + }; + + $scope.register = function() { + $scope.data.message = ''; + $scope.data.name = $scope.data.name.trim(); + $scope.data.email = $scope.data.email.trim(); + $scope.data.password = $scope.data.password.trim(); + $scope.data.password2 = $scope.data.password2.trim(); + // all fields complete + if($scope.data.name && $scope.data.email && $scope.data.password && $scope.data.password2) { + if($scope.data.password !== $scope.data.password2) { + $scope.data.message = 'Passwords do not match'; + $scope.data.password = $scope.data.password2 = ''; + return; + } + } + $http({ + url: '/api/user/register', + method: 'POST', + data: { + name: $scope.data.name, + email: $scope.data.email, + password: $scope.data.password + } + }).then((res) => { + $scope.data.user = { + name: $scope.data.name, + email: $scope.data.email + }; + $scope.data.password = $scope.data.password2 = ''; + currentUser.user = $scope.data.user; + currentUser.uniqueid = $scope.data.user.name; + }).catch((e) => { + console.log('ERROR', e); + $scope.data.message = 'ERROR ' + e.data; + $scope.data.password = $scope.data.password2 = ''; + }); + }; + + console.log('loginform init'); + if(!angular.equals(currentUser.user, {})) { + $scope.data.user = currentUser.user; + } + }); + +}) (window.angular); diff --git a/web/static/login.html b/web/static/login.html new file mode 100644 index 0000000..1d993b4 --- /dev/null +++ b/web/static/login.html @@ -0,0 +1,51 @@ + +
+
+ {{ data.message }} +
+ +
+ + + + + + + + + + + + +
Name
Password
+ + + + + + + + + + + + + + + + + + + + + +
Name
Email
Password
Confirm Password
+
+ +
+

Greetings {{ data.user.name }}

+

Email - {{ data.user.email }}

+

Order History

+

Order history will be here

+
+
diff --git a/web/static/media/stan.png b/web/static/media/stan.png new file mode 100644 index 0000000..694ab77 Binary files /dev/null and b/web/static/media/stan.png differ diff --git a/web/static/product.html b/web/static/product.html new file mode 100644 index 0000000..acf3dda --- /dev/null +++ b/web/static/product.html @@ -0,0 +1,24 @@ + +
+
+ {{ data.message }} +
+ +

{{ data.product.name }}

+
+ +
+
+ {{ data.product.description }} +
+
+ Price €{{ data.product.price }} + + Out of stock + + + Quantity + + +
+
diff --git a/web/static/splash.html b/web/static/splash.html new file mode 100644 index 0000000..c347a25 --- /dev/null +++ b/web/static/splash.html @@ -0,0 +1,4 @@ + +
+ Splash content will be here +