├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── disscussion.md │ └── feature_request.md ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── code ├── app-ui │ ├── .gitignore │ ├── README.md │ ├── app.js │ ├── bin │ │ └── www │ ├── kafka-events.js │ ├── keycloak.json-example │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── images │ │ │ ├── Logo-RedHat-A-Color-RGB.png │ │ │ ├── calendar.png │ │ │ ├── clipboard.png │ │ │ ├── clipboard2.png │ │ │ ├── clock.png │ │ │ ├── folder.png │ │ │ ├── profile.png │ │ │ ├── search-results-banner.png │ │ │ ├── user-profile-background.png │ │ │ ├── user-profile-background2.png │ │ │ └── user-profile-background3.png │ │ ├── javascripts │ │ │ ├── clipboard.js │ │ │ ├── clipboard.min.js │ │ │ ├── copybuttons.js │ │ │ ├── dashboard.js │ │ │ ├── jquery-3.3.1.min.js │ │ │ ├── jquery-3.3.1.slim.min.js │ │ │ └── topbar.js │ │ └── stylesheets │ │ │ ├── bootstrap-4.3.1-dist │ │ │ ├── css │ │ │ │ ├── bootstrap-grid.css │ │ │ │ ├── bootstrap-grid.css.map │ │ │ │ ├── bootstrap-grid.min.css │ │ │ │ ├── bootstrap-grid.min.css.map │ │ │ │ ├── bootstrap-reboot.css │ │ │ │ ├── bootstrap-reboot.css.map │ │ │ │ ├── bootstrap-reboot.min.css │ │ │ │ ├── bootstrap-reboot.min.css.map │ │ │ │ ├── bootstrap.css │ │ │ │ ├── bootstrap.css.map │ │ │ │ ├── bootstrap.min.css │ │ │ │ └── bootstrap.min.css.map │ │ │ └── js │ │ │ │ ├── bootstrap.bundle.js │ │ │ │ ├── bootstrap.bundle.js.map │ │ │ │ ├── bootstrap.bundle.min.js │ │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ │ ├── bootstrap.js │ │ │ │ ├── bootstrap.js.map │ │ │ │ ├── bootstrap.min.js │ │ │ │ └── bootstrap.min.js.map │ │ │ ├── dashboard.css │ │ │ ├── search-results.css │ │ │ ├── style.css │ │ │ ├── topbar.css │ │ │ ├── user-profile.css │ │ │ ├── user-profile2.css │ │ │ └── user-profile3.css │ ├── routes │ │ ├── board.js │ │ ├── index.js │ │ ├── profile.js │ │ ├── search.js │ │ └── shared.js │ └── views │ │ ├── board.pug │ │ ├── dashboard.pug │ │ ├── error.pug │ │ ├── info-page.pug │ │ ├── layout-items.pug │ │ ├── layout-topbar.pug │ │ ├── profile.pug │ │ ├── search.pug │ │ └── shared.pug ├── boards │ ├── .gitignore │ ├── README.md │ ├── boardsapi.yaml │ ├── controllers │ │ ├── boardsController.js │ │ ├── itemsController.js │ │ └── sharedItems.js │ ├── package-lock.json │ ├── package.json │ └── svc.js ├── censor │ └── README.md ├── context-scraper │ ├── .gitignore │ ├── README.md │ ├── api.js │ ├── package-lock.json │ ├── package.json │ └── svc.js ├── mailer │ └── README.md ├── public-exporter │ └── README.md ├── scratch │ └── README.md ├── search │ └── README.md ├── sms-receiver │ └── README.md └── userprofile │ ├── .gitignore │ ├── .mvn │ └── wrapper │ │ ├── MavenWrapperDownloader.java │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties │ ├── .s2i │ └── environment │ ├── README.md │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ ├── s2i-jvm.sh │ ├── s2i-native.sh │ ├── src │ ├── main │ │ ├── docker │ │ │ ├── Dockerfile.jvm │ │ │ ├── Dockerfile.jvm.fabric8 │ │ │ └── Dockerfile.native │ │ ├── java │ │ │ └── org │ │ │ │ └── microservices │ │ │ │ └── demo │ │ │ │ ├── jpa │ │ │ │ └── UserProfileJPA.java │ │ │ │ ├── json │ │ │ │ ├── UserProfile.java │ │ │ │ └── UserProfilePhoto.java │ │ │ │ ├── rest │ │ │ │ └── UserProfileResource.java │ │ │ │ └── service │ │ │ │ ├── ImageBase64Processor.java │ │ │ │ ├── UserProfileJPAServiceImpl.java │ │ │ │ ├── UserProfileService.java │ │ │ │ └── UserProfileServiceInMemoryImpl.java │ │ └── resources │ │ │ ├── META-INF │ │ │ ├── beans.xml │ │ │ └── resources │ │ │ │ └── index.html │ │ │ └── application.properties │ └── test │ │ └── java │ │ └── org │ │ └── microservices │ │ └── demo │ │ └── service │ │ ├── NativeUserProfileServiceIT.java │ │ └── UserProfileServiceTest.java │ ├── template.sh │ └── userprofile_v1_api.yaml ├── config ├── app │ ├── app-ui-fromsource.yaml │ ├── boards-fromsource.yaml │ ├── context-scraper-fromsource.yaml │ ├── userprofile-build.yaml │ ├── userprofile-deploy-all.yaml │ ├── userprofile-deploy-v2.yaml │ └── userprofile-deploy-v3.yaml ├── istio │ ├── authorization-boards-allow-all.yaml │ ├── authorization-boards-shared-lockdown.yaml │ ├── destinationrule-circuitbreaking.yaml │ ├── destinationrule-mtls.yaml │ ├── destinationrules-all.yaml │ ├── gateway.yaml │ ├── peer-authentication-mtls.yaml │ ├── request-authentication-boards-jwt.yaml │ ├── serviceentry-googleapis.yaml │ ├── serviceentry-keycloak.yaml │ ├── virtual-service-ingress-split.yaml │ ├── virtual-service-userprofile-50-50.yaml │ ├── virtual-service-userprofile-503.yaml │ ├── virtual-service-userprofile-90-10.yaml │ ├── virtual-service-userprofile-delay.yaml │ ├── virtual-service-userprofile-v1.yaml │ ├── virtual-service-userprofile-v3.yaml │ ├── virtual-services-all-v2.yaml │ └── virtual-services-default.yaml └── sso │ ├── sso-keycloak.yaml │ ├── sso-realm.yaml │ ├── sso-user1.yaml │ └── sso-user2.yaml └── design ├── FEATURES.md ├── README.md ├── highlevel-arch.png ├── ocp-arch.png ├── openshift-microservices.graffle ├── screenshots └── 2019-04-19_1042.png ├── sketches └── README.md └── workshop-arch.png /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. Deployment specifics or if you know which subcomponent this applies to you can lis that here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/disscussion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Disscussion 3 | about: Start a discussion about the design, architecture, or related technology 4 | title: '' 5 | labels: discussion 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Discussion Topic / Overview** 11 | [A basic description of what the discussion is about] 12 | 13 | **Kick it off** 14 | [Start the discussion here] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature-idea 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true, 3 | "quarkus.tools.validation.unknown.excluded": [ 4 | "*/mp-rest/providers/*/priority", 5 | "mp.openapi.schema.*", 6 | "defaultprofilestyle" 7 | ] 8 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | I'd love to have your help! There aren't a whole lot of rules around this right now. But here are a few. 3 | 4 | 1. Please make sure to track your changes against an open issue (you can write one if nothing exists). 5 | 2. Don't solve multiple things in the same PR - it's likely it will get rejected if too many changes are all grouped together 6 | 3. If you are making a major changes - e.g. APIs or look and feel - please start an issue conversation for design consensus before you get too far implementing changes. 7 | 8 | 9 | THANKS FOR PARTICIPATING!!! 10 | 11 | @dudash -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THIS IS EXAMPLE CODE FOR THE SERVICE MESH WORKSHOP 2 | We will not be merging anything back from this repo to the upstream it was forked from. It can follow it's own path. 3 | 4 | Also, note that this code intentionally has bugs, strange env vars, and unfinished parts - this is so that the workshop can demonstrate features of OpenShift and the OpenShift Service Mesh (Istio). This let's us examine use cases where the service mesh can aid in development as well as operational activities. 5 | 6 | The main services used in the workshop are: app-ui, boards, context-scraper, profile service, and Keycloak (SSO) 7 | 8 | [![OpenShift Version][openshift-heximage]][openshift-url] 9 | 10 | You can find [the corresponding labs here](http://redhatgov.io/workshops/openshift_service_mesh/) 11 | 12 | # Microservices 13 | Microservices, also known as the microservice architecture, is a software development technique that structures an application as a collection of loosely coupled services. Microservice architectures enable the continuous delivery/deployment/scaling of complex applications. 14 | 15 | This git repo showcases an app built using the microservice architecture with several intentionally simple components. The goal is to showcase an example way to develop and manage microservices using a container platform and service mesh. 16 | 17 | ## Why microservices? 18 | Agility. Deliver application updates faster. Isolate and fix bugs easier. Done right, a microservices architecture will you help to meet several important non-functional requirements for your software: 19 | * scalability 20 | * performance 21 | * reliability 22 | * resiliency 23 | * extensibility 24 | * availability 25 | 26 | ## Current screenshot 27 | ![Screenshot](design/screenshots/2019-04-19_1042.png?raw=true) 28 | 29 | 30 | ## Here's the initial design for the architecture: 31 | 32 | ![Diagram](design/highlevel-arch.png) 33 | *In the above diagram web app users are accessing the APP UI service which in turns calls chains of microservices on the backend. Optionally the backend services have calls to API services managed via 3scale (and future access to the services from mobile apps could go through the 3scale API management capability as well). A single sign on capability provides security around user access to the application via OpenID Connect (OIDC) and OAuth2. The Istio service mesh is shown too - it provides core capabilities for traffic management and security of the services as well as detailed observation into the application's operational status. All of this is running on top of an OpenShift cluster. (Additional service interactions and deployment details are in other diagrams).* 34 | 35 | ![Diagram](design/ocp-arch.png) 36 | *The above diagram shows how the services are related and additionally how they are abstracted from the underlying infrastructure (compute and storage) when deployed on top of an OpenShift cluster. (The abstraction means this can be run in AWS, GCP, Azure, on-prem, or in some hybrid combination).* 37 | 38 | ###### :information_source: This example is based on OpenShift Container Platform version 4.3. 39 | 40 | ## How to run this? 41 | We recommend using RHPDS (if you have access) to run this workshop. 42 | 43 | To install everything: 44 | - [Follow instructions here](./deployment/workshop/README.md) 45 | 46 | Once you have the cluster pre-reqs up and running, the labs will walk you through install and using the application. 47 | 48 | 49 | ## About the code / software architecture 50 | The parts in action here are: 51 | * A set of microservices that together provide full application capability for a cut and paste board (in code folder) 52 | * Key platform components that enable this example: 53 | * container building via s2i 54 | * service load balancing 55 | * service autoscaling 56 | * service health checks and recovery 57 | * dynamic storage allocation and persistent volume mapping 58 | * Kubernetes operators to manage middleware components (e.g. Kafka) 59 | * advanced service traffic management via Istio 60 | * additional service observability via Istio 61 | * Middleware components in this example: 62 | * API management and metrics (on the external facing APIs) 63 | * authorization and application security via SSO 64 | * Kafka for scalable messaging 65 | 66 | 67 | ## References, useful links, good videos to check out 68 | ### Microservices 69 | * [Microservices at Spotify Talk](https://www.youtube.com/watch?v=7LGPeBgNFuU) 70 | * [Microservices at Uber Talk](https://www.youtube.com/watch?v=kb-m2fasdDY) 71 | * [Mastering Chaos Netflix Talk](https://youtu.be/CZ3wIuvmHeM) 72 | * [Red Hat Developer's Learning - Microservices](https://developers.redhat.com/learn/microservices/) 73 | ### Istio Service Mesh 74 | * [What is Istio?](https://istio.io/docs/concepts/what-is-istio/) 75 | * [Red Hat Developer's Istio Free Book](https://developers.redhat.com/books/introducing-istio-service-mesh-microservices/) 76 | * [Free Hands-on with Istio](https://learn.openshift.com/servicemesh) 77 | ### Single Sign On 78 | * [Keycloak SSO](https://www.keycloak.org/) 79 | 80 | 81 | ## License 82 | Apache 2.0. 83 | 84 | 85 | [openshift-heximage]: https://img.shields.io/badge/openshift-4.3-BB261A.svg 86 | [openshift-url]: https://docs.openshift.com/container-platform/4.3/welcome/index.html -------------------------------------------------------------------------------- /code/app-ui/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless 75 | 76 | # nodeshift and odo 77 | tmp/* 78 | .odo/* 79 | .odo/odo-file-index.json -------------------------------------------------------------------------------- /code/app-ui/README.md: -------------------------------------------------------------------------------- 1 | # APP-UI 2 | ## The app's user interface 3 | This microservice provides the main user interface into the application. 4 | 5 | ## Developer instructions 6 | 7 | ### Env Vars 8 | - PORT, port to run the service (defaults to 8080 in PROD, 3000 in DEV) 9 | - HTTP_PROTOCOL, default='http://' 10 | - SERVICE_NAME, default='app-ui' 11 | - BOARDS_SVC_HOST, default='boards' 12 | - BOARDS_SVC_PORT, default='8080' 13 | - PROFILE_SVC_HOST, default='profile' 14 | - PROFILE_SVC_PORT, default='8080' 15 | - SSO_SVC_HOST, default='auth-sso73-x509' 16 | - SSO_SVC_PORT, default='8443' 17 | - SESSION_SECRET, default='pleasechangeme' 18 | - FAKE_USER, default=false (this turns off SSO checks and injects a fake user) 19 | - NODE_TLS_REJECT_UNAUTHORIZED, default=unset (this tells node.js to reject or accept self-signed certs) 20 | 21 | ### Local Installation / Run / Test 22 | ```bash 23 | $ npm install 24 | ``` 25 | 26 | Start the service: 27 | ```bash 28 | $ npm run-script dev 29 | ``` 30 | 31 | ### Deploy / Run / Test Local Code to OpenShift - The easy way 32 | We can use odo to do our OpenShift deployments and iterations on code/test: 33 | ```bash 34 | odo component create nodejs app-ui --now 35 | odo url create --port 8080 36 | odo config set --env BOARDS_SVC_HOST=boards-app 37 | odo push 38 | ``` 39 | 40 | ### Deploy / Run / Test Local Code to OpenShift - The complicated but configurable YAML way 41 | You can use a template to create all the build and deployment resources for OpenShift. Here's an example that overrides the defaults: 42 | ```bash 43 | oc new-app -f ../../deployment/install/microservices/openshift-configuration/app-ui-fromsource.yaml \ 44 | -p APPLICATION_NAME=app-ui \ 45 | -p NODEJS_VERSION_TAG=10 \ 46 | -p GIT_BRANCH=develop \ 47 | -p GIT_URI=https://github.com/dudash/openshift-microservices.git 48 | ``` 49 | Note: the template uses S2I which pulls from git and builds the container image from source code then deploys. 50 | 51 | ### Building a container image for this service 52 | You can use [s2i][2] to build this into a container image. For example to use the OpenShift runtimes node.js as our base: 53 | ```bash 54 | rm -rf node_modules 55 | s2i build . registry.access.redhat.com/ubi8/nodejs-12 openshift-microservices-app-ui --loglevel 3 56 | ``` 57 | Note: we remove the node_modules to avoid conflicts during the build process 58 | 59 | ### Developer Tips 60 | Useful tool for converting HTML examples to pug files: [https://html2jade.org/][1] 61 | 62 | [1]: https://html2jade.org/ 63 | [2]: https://github.com/openshift/source-to-image/releases 64 | 65 | ### Other Notes 66 | You will need to deploy the boards microservice for this app-ui to function properly. 67 | 68 | If you have a self signed cert on your keycloak SSO you might need to tell node.js that's OK: 69 | ```export set NODE_TLS_REJECT_UNAUTHORIZED=0``` 70 | 71 | To debug your app in OpenShift you can use the following env vars: 72 | * DEBUG = sso, app 73 | * NODE_DEBUG = request -------------------------------------------------------------------------------- /code/app-ui/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('app-ui:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '8080'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /code/app-ui/kafka-events.js: -------------------------------------------------------------------------------- 1 | // Created by: Jason Dudash 2 | // https://github.com/dudash 3 | // 4 | // (C) 2019 5 | // Released under the terms of Apache-2.0 License 6 | // 7 | // The events module is to make it easy to publish a Kafka events 8 | // in the app-ui service. This object will create a connection 9 | // to the Kafka broker and the sendPayload function does the rest. 10 | // Note: we aren't enforcing any type in the data format (JSON not Avro). 11 | // 12 | // https://www.npmjs.com/package/kafka-node#kafkaclient 13 | // 14 | var kafka = require('kafka-node'), 15 | uuid = require('uuid') 16 | var client, producer, isReady 17 | const USER_PAYLOAD_TOPIC = 'microservicesdemo.tracking.user-level' 18 | const SERVICE_PAYLOAD_TOPIC = 'microservicesdemo.tracking.service-level' 19 | 20 | const init = () => { 21 | if (producer) { 22 | console.log('not recreating the Kafka producer, it already exists') 23 | return 24 | } else { console.log('creating the Kafka producer') } 25 | isReady = false 26 | client = new kafka.KafkaClient({kafkaHost: process.env.KAFKA_HOST || '127.0.0.1:9092'}) 27 | producer = new kafka.HighLevelProducer(client) 28 | producer.on('ready', function() { 29 | console.log('Kafka is connected and producer is ready.') 30 | isReady = true 31 | }) 32 | producer.on('error', function(error) { 33 | console.error('Kafka producer error:' + error) 34 | }) 35 | } 36 | 37 | const sender = { 38 | sendUserPayload: ({ type, userId, sessionId, data }, callback = () => {}) => { 39 | if (!isReady) { return callback(new Error(`Kafka not ready yet...`)) } 40 | if (!userId) { return callback(new Error(`missing userId`)) } 41 | const event = { 42 | id: uuid.v4(), 43 | timestamp: Date.now(), 44 | type: type, 45 | userId: userId, 46 | sessionId: sessionId, 47 | data: data 48 | } 49 | const buffer = new Buffer.from(JSON.stringify(event)) 50 | const payloads = [ 51 | { 52 | topic: USER_PAYLOAD_TOPIC, 53 | messages: buffer, 54 | attributes: 1 /* Use GZip compression for the payloads */ 55 | } 56 | ] 57 | producer.send(payloads, callback) 58 | //console.log('Kafka user payload sent') 59 | }, 60 | sendServicePayload: ({ type, data }, callback = () => {}) => { 61 | if (!isReady) { return callback(new Error(`Kafka not ready yet...`)) } 62 | const event = { 63 | id: uuid.v4(), 64 | timestamp: Date.now(), 65 | type: type, 66 | data: data 67 | } 68 | const buffer = new Buffer.from(JSON.stringify(event)) 69 | const payloads = [ 70 | { 71 | topic: SERVICE_PAYLOAD_TOPIC, 72 | messages: buffer, 73 | attributes: 1 /* Use GZip compression for the payloads */ 74 | } 75 | ] 76 | producer.send(payloads, callback) 77 | //console.log('Kafka service payload sent') 78 | } 79 | } 80 | 81 | module.exports = { 82 | init, 83 | sender 84 | } 85 | -------------------------------------------------------------------------------- /code/app-ui/keycloak.json-example: -------------------------------------------------------------------------------- 1 | { 2 | "realm": "microservices-demo", 3 | "auth-server-url": "https://keycloak-sso-shared.apps.leonardo.nub3s.io/auth/", 4 | "ssl-required": "external", 5 | "resource": "client-app", 6 | "public-client": true, 7 | "verify-token-audience": true, 8 | "use-resource-role-mappings": true, 9 | "confidential-port": 0 10 | } -------------------------------------------------------------------------------- /code/app-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-ui", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www", 7 | "build": "echo \"Info: no build steps here - in the future we might minify\"", 8 | "dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 DEBUG=app,sso PORT=3000 BOARDS_SVC_HOST=localhost BOARDS_SVC_PORT=3001 PROFILE_SVC_HOST=localhost PROFILE_SVC_PORT=8080 nodemon ./bin/www", 9 | "audit": "snyk test" 10 | }, 11 | "author": "dudash", 12 | "license": "Apache-2.0", 13 | "bugs": { 14 | "url": "https://github.com/dudash/openshift-microservices/issues" 15 | }, 16 | "homepage": "https://github.com/dudash/openshift-microservices#readme", 17 | "dependencies": { 18 | "cookie-parser": "^1.4.4", 19 | "cors": "~2.8.5", 20 | "debug": "^4.1.1", 21 | "express": "~4.16.4", 22 | "express-session": "^1.17.0", 23 | "http-errors": "~1.6.3", 24 | "kafka-node": "^4.1.3", 25 | "keycloak-connect": "^18.0.2", 26 | "moment": "~2.24.0", 27 | "pug": "^2.0.4", 28 | "request": "^2.88.2", 29 | "request-promise": "^4.2.5", 30 | "uuid": "^3.4.0" 31 | }, 32 | "devDependencies": { 33 | "nodemon": "^1.19.4", 34 | "snyk": "^1.294.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/app-ui/public/images/Logo-RedHat-A-Color-RGB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHatGov/service-mesh-workshop-code/3bda71039bc1e12e106e77c6f687e55ca991afd3/code/app-ui/public/images/Logo-RedHat-A-Color-RGB.png -------------------------------------------------------------------------------- /code/app-ui/public/images/calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHatGov/service-mesh-workshop-code/3bda71039bc1e12e106e77c6f687e55ca991afd3/code/app-ui/public/images/calendar.png -------------------------------------------------------------------------------- /code/app-ui/public/images/clipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHatGov/service-mesh-workshop-code/3bda71039bc1e12e106e77c6f687e55ca991afd3/code/app-ui/public/images/clipboard.png -------------------------------------------------------------------------------- /code/app-ui/public/images/clipboard2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHatGov/service-mesh-workshop-code/3bda71039bc1e12e106e77c6f687e55ca991afd3/code/app-ui/public/images/clipboard2.png -------------------------------------------------------------------------------- /code/app-ui/public/images/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHatGov/service-mesh-workshop-code/3bda71039bc1e12e106e77c6f687e55ca991afd3/code/app-ui/public/images/clock.png -------------------------------------------------------------------------------- /code/app-ui/public/images/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHatGov/service-mesh-workshop-code/3bda71039bc1e12e106e77c6f687e55ca991afd3/code/app-ui/public/images/folder.png -------------------------------------------------------------------------------- /code/app-ui/public/images/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHatGov/service-mesh-workshop-code/3bda71039bc1e12e106e77c6f687e55ca991afd3/code/app-ui/public/images/profile.png -------------------------------------------------------------------------------- /code/app-ui/public/images/search-results-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHatGov/service-mesh-workshop-code/3bda71039bc1e12e106e77c6f687e55ca991afd3/code/app-ui/public/images/search-results-banner.png -------------------------------------------------------------------------------- /code/app-ui/public/images/user-profile-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHatGov/service-mesh-workshop-code/3bda71039bc1e12e106e77c6f687e55ca991afd3/code/app-ui/public/images/user-profile-background.png -------------------------------------------------------------------------------- /code/app-ui/public/images/user-profile-background2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHatGov/service-mesh-workshop-code/3bda71039bc1e12e106e77c6f687e55ca991afd3/code/app-ui/public/images/user-profile-background2.png -------------------------------------------------------------------------------- /code/app-ui/public/images/user-profile-background3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedHatGov/service-mesh-workshop-code/3bda71039bc1e12e106e77c6f687e55ca991afd3/code/app-ui/public/images/user-profile-background3.png -------------------------------------------------------------------------------- /code/app-ui/public/javascripts/copybuttons.js: -------------------------------------------------------------------------------- 1 | var clipboardDemos = new Clipboard('.btn'); 2 | 3 | function showTooltip(elem, msg) { 4 | // elem.setAttribute('class', 'btn tooltipped tooltipped-s'); 5 | // elem.setAttribute('aria-label', msg); 6 | elem.setAttribute('data-placement', 'left'); 7 | elem.setAttribute('data-toggle', 'popover'); 8 | elem.setAttribute('data-trigger', 'focus'); 9 | elem.setAttribute('data-content', msg); 10 | } 11 | 12 | clipboardDemos.on('success', function(e) { 13 | e.clearSelection(); 14 | // console.info('Action:', e.action); 15 | // console.info('Text:', e.text); 16 | // console.info('Trigger:', e.trigger); 17 | showTooltip(e.trigger, 'Copied!'); 18 | }); 19 | 20 | clipboardDemos.on('error', function(e) { 21 | // console.error('Action:', e.action); 22 | // console.error('Trigger:', e.trigger); 23 | showTooltip(e.trigger, 'Press Ctrl+C'); 24 | }); 25 | -------------------------------------------------------------------------------- /code/app-ui/public/javascripts/dashboard.js: -------------------------------------------------------------------------------- 1 | $('#newBoardModal').on('show.bs.modal', function (event) { 2 | //var button = $(event.relatedTarget) // Button that triggered the modal 3 | //var recipient = button.data('whatever') // Extract info from data-* attributes 4 | 5 | // If necessary, you could initiate an AJAX request here (and then do the updating in a callback). 6 | // Update the modal's content. We'll use jQuery here, but you could use a data binding library or other methods instead. 7 | var modal = $(this) 8 | 9 | // setup a validator 10 | $('#newboardSubmitButton').prop('disabled', true); 11 | $('.modal-body input').off('keyup'); 12 | $('.modal-body input').on('keyup', validate); 13 | 14 | // clear out prior input values 15 | modal.find('.modal-body input').val('') 16 | modal.find('.modal-body textarea').val('') 17 | modal.find('.modal-body input:checkbox').prop("checked", false) 18 | }) 19 | 20 | function validate() { 21 | if ($('#newboardName').val()) { 22 | $('#newboardSubmitButton').prop('disabled', false); 23 | } else { 24 | $('#newboardSubmitButton').prop('disabled', true); 25 | } 26 | } 27 | 28 | // $('#newBoardForm').click(function(e){ 29 | // e.preventDefault(); 30 | // alert($('#newboardName').val()); 31 | // 32 | // $.post('http://path/to/post', 33 | // $('#newBoardForm').serialize(), 34 | // function(data, status, xhr){ 35 | // // do something here with response; 36 | // }); 37 | // 38 | // }) -------------------------------------------------------------------------------- /code/app-ui/public/javascripts/topbar.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 'use strict' 3 | $('[data-toggle="offcanvas"]').on('click', function () { 4 | $('.offcanvas-collapse').toggleClass('open') 5 | }) 6 | }) -------------------------------------------------------------------------------- /code/app-ui/public/stylesheets/bootstrap-4.3.1-dist/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 28 | font-size: 1rem; 29 | font-weight: 400; 30 | line-height: 1.5; 31 | color: #212529; 32 | text-align: left; 33 | background-color: #fff; 34 | } 35 | 36 | [tabindex="-1"]:focus { 37 | outline: 0 !important; 38 | } 39 | 40 | hr { 41 | box-sizing: content-box; 42 | height: 0; 43 | overflow: visible; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | abbr[title], 57 | abbr[data-original-title] { 58 | text-decoration: underline; 59 | -webkit-text-decoration: underline dotted; 60 | text-decoration: underline dotted; 61 | cursor: help; 62 | border-bottom: 0; 63 | -webkit-text-decoration-skip-ink: none; 64 | text-decoration-skip-ink: none; 65 | } 66 | 67 | address { 68 | margin-bottom: 1rem; 69 | font-style: normal; 70 | line-height: inherit; 71 | } 72 | 73 | ol, 74 | ul, 75 | dl { 76 | margin-top: 0; 77 | margin-bottom: 1rem; 78 | } 79 | 80 | ol ol, 81 | ul ul, 82 | ol ul, 83 | ul ol { 84 | margin-bottom: 0; 85 | } 86 | 87 | dt { 88 | font-weight: 700; 89 | } 90 | 91 | dd { 92 | margin-bottom: .5rem; 93 | margin-left: 0; 94 | } 95 | 96 | blockquote { 97 | margin: 0 0 1rem; 98 | } 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | small { 106 | font-size: 80%; 107 | } 108 | 109 | sub, 110 | sup { 111 | position: relative; 112 | font-size: 75%; 113 | line-height: 0; 114 | vertical-align: baseline; 115 | } 116 | 117 | sub { 118 | bottom: -.25em; 119 | } 120 | 121 | sup { 122 | top: -.5em; 123 | } 124 | 125 | a { 126 | color: #007bff; 127 | text-decoration: none; 128 | background-color: transparent; 129 | } 130 | 131 | a:hover { 132 | color: #0056b3; 133 | text-decoration: underline; 134 | } 135 | 136 | a:not([href]):not([tabindex]) { 137 | color: inherit; 138 | text-decoration: none; 139 | } 140 | 141 | a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { 142 | color: inherit; 143 | text-decoration: none; 144 | } 145 | 146 | a:not([href]):not([tabindex]):focus { 147 | outline: 0; 148 | } 149 | 150 | pre, 151 | code, 152 | kbd, 153 | samp { 154 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 155 | font-size: 1em; 156 | } 157 | 158 | pre { 159 | margin-top: 0; 160 | margin-bottom: 1rem; 161 | overflow: auto; 162 | } 163 | 164 | figure { 165 | margin: 0 0 1rem; 166 | } 167 | 168 | img { 169 | vertical-align: middle; 170 | border-style: none; 171 | } 172 | 173 | svg { 174 | overflow: hidden; 175 | vertical-align: middle; 176 | } 177 | 178 | table { 179 | border-collapse: collapse; 180 | } 181 | 182 | caption { 183 | padding-top: 0.75rem; 184 | padding-bottom: 0.75rem; 185 | color: #6c757d; 186 | text-align: left; 187 | caption-side: bottom; 188 | } 189 | 190 | th { 191 | text-align: inherit; 192 | } 193 | 194 | label { 195 | display: inline-block; 196 | margin-bottom: 0.5rem; 197 | } 198 | 199 | button { 200 | border-radius: 0; 201 | } 202 | 203 | button:focus { 204 | outline: 1px dotted; 205 | outline: 5px auto -webkit-focus-ring-color; 206 | } 207 | 208 | input, 209 | button, 210 | select, 211 | optgroup, 212 | textarea { 213 | margin: 0; 214 | font-family: inherit; 215 | font-size: inherit; 216 | line-height: inherit; 217 | } 218 | 219 | button, 220 | input { 221 | overflow: visible; 222 | } 223 | 224 | button, 225 | select { 226 | text-transform: none; 227 | } 228 | 229 | select { 230 | word-wrap: normal; 231 | } 232 | 233 | button, 234 | [type="button"], 235 | [type="reset"], 236 | [type="submit"] { 237 | -webkit-appearance: button; 238 | } 239 | 240 | button:not(:disabled), 241 | [type="button"]:not(:disabled), 242 | [type="reset"]:not(:disabled), 243 | [type="submit"]:not(:disabled) { 244 | cursor: pointer; 245 | } 246 | 247 | button::-moz-focus-inner, 248 | [type="button"]::-moz-focus-inner, 249 | [type="reset"]::-moz-focus-inner, 250 | [type="submit"]::-moz-focus-inner { 251 | padding: 0; 252 | border-style: none; 253 | } 254 | 255 | input[type="radio"], 256 | input[type="checkbox"] { 257 | box-sizing: border-box; 258 | padding: 0; 259 | } 260 | 261 | input[type="date"], 262 | input[type="time"], 263 | input[type="datetime-local"], 264 | input[type="month"] { 265 | -webkit-appearance: listbox; 266 | } 267 | 268 | textarea { 269 | overflow: auto; 270 | resize: vertical; 271 | } 272 | 273 | fieldset { 274 | min-width: 0; 275 | padding: 0; 276 | margin: 0; 277 | border: 0; 278 | } 279 | 280 | legend { 281 | display: block; 282 | width: 100%; 283 | max-width: 100%; 284 | padding: 0; 285 | margin-bottom: .5rem; 286 | font-size: 1.5rem; 287 | line-height: inherit; 288 | color: inherit; 289 | white-space: normal; 290 | } 291 | 292 | progress { 293 | vertical-align: baseline; 294 | } 295 | 296 | [type="number"]::-webkit-inner-spin-button, 297 | [type="number"]::-webkit-outer-spin-button { 298 | height: auto; 299 | } 300 | 301 | [type="search"] { 302 | outline-offset: -2px; 303 | -webkit-appearance: none; 304 | } 305 | 306 | [type="search"]::-webkit-search-decoration { 307 | -webkit-appearance: none; 308 | } 309 | 310 | ::-webkit-file-upload-button { 311 | font: inherit; 312 | -webkit-appearance: button; 313 | } 314 | 315 | output { 316 | display: inline-block; 317 | } 318 | 319 | summary { 320 | display: list-item; 321 | cursor: pointer; 322 | } 323 | 324 | template { 325 | display: none; 326 | } 327 | 328 | [hidden] { 329 | display: none !important; 330 | } 331 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /code/app-ui/public/stylesheets/bootstrap-4.3.1-dist/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /code/app-ui/public/stylesheets/dashboard.css: -------------------------------------------------------------------------------- 1 | .jumbotron { 2 | padding-top: 3rem; 3 | padding-bottom: 3rem; 4 | margin-bottom: 0; 5 | background-color: #fff; 6 | } 7 | @media (min-width: 768px) { 8 | .jumbotron { 9 | padding-top: 6rem; 10 | padding-bottom: 6rem; 11 | } 12 | } 13 | 14 | .jumbotron p:last-child { 15 | margin-bottom: 0; 16 | } 17 | 18 | .jumbotron-heading { 19 | font-weight: 300; 20 | } 21 | 22 | .jumbotron .container { 23 | max-width: 40rem; 24 | } 25 | 26 | footer { 27 | padding-top: 3rem; 28 | padding-bottom: 3rem; 29 | } 30 | 31 | footer p { 32 | margin-bottom: .25rem; 33 | } 34 | -------------------------------------------------------------------------------- /code/app-ui/public/stylesheets/search-results.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | font-family: 'Open Sans', sans-serif; 4 | } 5 | 6 | .jumbotron { 7 | background-image: url("/images/search-results-banner.png"); 8 | background-size: cover; 9 | } 10 | -------------------------------------------------------------------------------- /code/app-ui/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font-family: 'Open Sans', sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | 10 | #icon { 11 | background:url(/images/clipboard.png) left top; 12 | width:20px; 13 | height:20px; 14 | padding-left: 24px; 15 | background-repeat: no-repeat; 16 | } -------------------------------------------------------------------------------- /code/app-ui/public/stylesheets/topbar.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | overflow-x: hidden; /* Prevent scroll on narrow devices */ 4 | } 5 | 6 | body { 7 | padding-top: 56px; 8 | font-family: 'Open Sans', sans-serif; 9 | } 10 | 11 | @media (max-width: 991.98px) { 12 | .offcanvas-collapse { 13 | position: fixed; 14 | top: 56px; /* Height of navbar */ 15 | bottom: 0; 16 | left: 100%; 17 | width: 100%; 18 | padding-right: 1rem; 19 | padding-left: 1rem; 20 | overflow-y: auto; 21 | visibility: hidden; 22 | background-color: #343a40; 23 | transition: visibility .3s ease-in-out, -webkit-transform .3s ease-in-out; 24 | transition: transform .3s ease-in-out, visibility .3s ease-in-out; 25 | transition: transform .3s ease-in-out, visibility .3s ease-in-out, -webkit-transform .3s ease-in-out; 26 | } 27 | .offcanvas-collapse.open { 28 | visibility: visible; 29 | -webkit-transform: translateX(-100%); 30 | transform: translateX(-100%); 31 | } 32 | } 33 | 34 | .nav-scroller { 35 | position: relative; 36 | z-index: 2; 37 | height: 2.75rem; 38 | overflow-y: hidden; 39 | } 40 | 41 | .nav-scroller .nav { 42 | display: -ms-flexbox; 43 | display: flex; 44 | -ms-flex-wrap: nowrap; 45 | flex-wrap: nowrap; 46 | padding-bottom: 1rem; 47 | margin-top: -1px; 48 | overflow-x: auto; 49 | color: rgba(255, 255, 255, .75); 50 | text-align: center; 51 | white-space: nowrap; 52 | -webkit-overflow-scrolling: touch; 53 | } 54 | 55 | .nav-underline .nav-link { 56 | padding-top: .75rem; 57 | padding-bottom: .75rem; 58 | font-size: .875rem; 59 | color: #6c757d; 60 | } 61 | 62 | .nav-underline .nav-link:hover { 63 | color: #007bff; 64 | } 65 | 66 | .nav-underline .active { 67 | font-weight: 500; 68 | color: #343a40; 69 | } 70 | 71 | .text-white-50 { color: rgba(255, 255, 255, .5); } 72 | 73 | .bg-purple { background-color: #6f42c1; } 74 | .bg-burntorange { background-color: #d46102; } 75 | .bg-brown { background-color: #522500; } 76 | 77 | .lh-100 { line-height: 1; } 78 | .lh-125 { line-height: 1.25; } 79 | .lh-150 { line-height: 1.5; } 80 | 81 | .modal-confirm { 82 | color: #434e65; 83 | width: 525px; 84 | } 85 | .modal-confirm .modal-content { 86 | padding: 20px; 87 | font-size: 16px; 88 | border-radius: 5px; 89 | border: none; 90 | } 91 | .modal-confirm .modal-header { 92 | background: #e85e6c; 93 | border-bottom: none; 94 | position: relative; 95 | text-align: center; 96 | margin: -20px -20px 0; 97 | border-radius: 5px 5px 0 0; 98 | padding: 35px; 99 | } 100 | .modal-confirm h4 { 101 | text-align: center; 102 | font-size: 36px; 103 | margin: 10px 0; 104 | } 105 | .modal-confirm .form-control, .modal-confirm .btn { 106 | min-height: 40px; 107 | border-radius: 3px; 108 | } 109 | .modal-confirm .close { 110 | position: absolute; 111 | top: 15px; 112 | right: 15px; 113 | color: #fff; 114 | text-shadow: none; 115 | opacity: 0.5; 116 | } 117 | .modal-confirm .close:hover { 118 | opacity: 0.8; 119 | } 120 | .modal-confirm .icon-box { 121 | color: #fff; 122 | width: 95px; 123 | height: 95px; 124 | display: inline-block; 125 | border-radius: 50%; 126 | z-index: 9; 127 | border: 5px solid #fff; 128 | padding: 15px; 129 | text-align: center; 130 | } 131 | .modal-confirm .icon-box i { 132 | font-size: 58px; 133 | margin: -2px 0 0 -2px; 134 | } 135 | .modal-confirm.modal-dialog { 136 | margin-top: 80px; 137 | } 138 | .modal-confirm .btn { 139 | color: #fff; 140 | border-radius: 4px; 141 | background: #eeb711; 142 | text-decoration: none; 143 | transition: all 0.4s; 144 | line-height: normal; 145 | border-radius: 30px; 146 | margin-top: 10px; 147 | padding: 6px 20px; 148 | min-width: 150px; 149 | border: none; 150 | } 151 | .modal-confirm .btn:hover, .modal-confirm .btn:focus { 152 | background: #eda645; 153 | outline: none; 154 | } 155 | 156 | .info-text 157 | { 158 | text-align: left; 159 | margin-left: 50px; 160 | } 161 | 162 | /* 163 | Top bar login button and form theme 164 | from here: https://bootsnipp.com/snippets/DV3m4 165 | */ 166 | 167 | #login-dp{ 168 | min-width: 250px; 169 | padding: 14px 14px 0; 170 | overflow:hidden; 171 | background-color:rgba(255,255,255,.8); 172 | } 173 | #login-dp .help-block{ 174 | font-size:12px 175 | } 176 | #login-dp .bottom{ 177 | background-color:rgba(255,255,255,.8); 178 | border-top:1px solid #ddd; 179 | clear:both; 180 | padding:14px; 181 | } 182 | #login-dp .social-buttons{ 183 | margin:12px 0 184 | } 185 | #login-dp .social-buttons a{ 186 | width: 49%; 187 | } 188 | #login-dp .form-group { 189 | margin-bottom: 10px; 190 | } 191 | .btn-fb{ 192 | color: #fff; 193 | background-color:#3b5998; 194 | } 195 | .btn-fb:hover{ 196 | color: #fff; 197 | background-color:#496ebc 198 | } 199 | .btn-tw{ 200 | color: #fff; 201 | background-color:#55acee; 202 | } 203 | .btn-tw:hover{ 204 | color: #fff; 205 | background-color:#59b5fa; 206 | } 207 | @media(max-width:768px){ 208 | #login-dp{ 209 | background-color: inherit; 210 | color: #fff; 211 | } 212 | #login-dp .bottom{ 213 | background-color: inherit; 214 | border-top:0 none; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /code/app-ui/public/stylesheets/user-profile.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | font-family: 'Open Sans', sans-serif; 4 | } 5 | 6 | .jumbotron { 7 | background-image: url("/images/user-profile-background.png"); 8 | background-size: cover; 9 | } 10 | 11 | .user-profile img.user-profile-image-bkg 12 | { 13 | z-index: 0; 14 | width: 100%; 15 | margin-bottom: 10px; 16 | position:absolute; 17 | } 18 | 19 | .user-profile-image 20 | { 21 | margin: -90px 10px 0px 50px; 22 | z-index: 9; 23 | width: 20%; 24 | } 25 | 26 | .user-profile-text 27 | { 28 | text-align: left; 29 | margin-left: 50px; 30 | } 31 | 32 | @media (max-width:768px) 33 | { 34 | .user-profile-text>h1 35 | { 36 | font-weight: 700; 37 | font-size:16px; 38 | } 39 | 40 | .user-profile-image 41 | { 42 | margin: -45px 10px 0px 25px; 43 | z-index: 9; 44 | width: 20%; 45 | } 46 | } -------------------------------------------------------------------------------- /code/app-ui/public/stylesheets/user-profile2.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | font-family: 'Open Sans', sans-serif; 4 | } 5 | 6 | .jumbotron { 7 | background-image: url("/images/user-profile-background2.png"); 8 | background-size: cover; 9 | } 10 | 11 | .user-profile img.user-profile-image-bkg 12 | { 13 | z-index: 0; 14 | width: 100%; 15 | margin-bottom: 10px; 16 | position:absolute; 17 | } 18 | 19 | .user-profile-image 20 | { 21 | margin: -90px 10px 0px 50px; 22 | z-index: 9; 23 | width: 20%; 24 | } 25 | 26 | .user-profile-text 27 | { 28 | text-align: left; 29 | margin-left: 50px; 30 | } 31 | 32 | @media (max-width:768px) 33 | { 34 | .user-profile-text>h1 35 | { 36 | font-weight: 700; 37 | font-size:16px; 38 | } 39 | 40 | .user-profile-image 41 | { 42 | margin: -45px 10px 0px 25px; 43 | z-index: 9; 44 | width: 20%; 45 | } 46 | } -------------------------------------------------------------------------------- /code/app-ui/public/stylesheets/user-profile3.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | font-family: 'Open Sans', sans-serif; 4 | } 5 | 6 | .jumbotron { 7 | background-image: url("/images/user-profile-background3.png"); 8 | background-size: cover; 9 | } 10 | 11 | .user-profile img.user-profile-image-bkg 12 | { 13 | z-index: 0; 14 | width: 100%; 15 | margin-bottom: 10px; 16 | position:absolute; 17 | } 18 | 19 | .user-profile-image 20 | { 21 | margin: -90px 10px 0px 50px; 22 | z-index: 9; 23 | width: 20%; 24 | } 25 | 26 | .user-profile-text 27 | { 28 | text-align: left; 29 | margin-left: 50px; 30 | } 31 | 32 | @media (max-width:768px) 33 | { 34 | .user-profile-text>h1 35 | { 36 | font-weight: 700; 37 | font-size:16px; 38 | } 39 | 40 | .user-profile-image 41 | { 42 | margin: -45px 10px 0px 25px; 43 | z-index: 9; 44 | width: 20%; 45 | } 46 | } -------------------------------------------------------------------------------- /code/app-ui/routes/board.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var router = express.Router() 3 | var moment = require('moment') 4 | var request = require('request-promise') 5 | 6 | /* GET board's page. */ 7 | router.get('/:boardId', function(req, res, next) { 8 | var user = res.locals.user 9 | const boardsGetURI = req.HTTP_PROTOCOL + req.BOARDS_SVC_HOST + ':' + req.BOARDS_SVC_PORT + '/' + user + '/boards/' + req.params.boardId 10 | req.debug('GET from boards SVC at: ' + boardsGetURI) 11 | var request_get_options = { 12 | method: 'GET', 13 | uri: boardsGetURI, 14 | headers: { 15 | 'user-agent': req.header('user-agent'), 16 | 'Authorization': 'Bearer ' + res.locals.authToken, 17 | 'x-request-id': req.header('x-request-id'), 18 | 'x-b3-traceid': req.header('x-b3-traceid'), 19 | 'x-b3-spanid': req.header('x-b3-spanid'), 20 | 'x-b3-parentspanid': req.header('x-b3-parentspanid'), 21 | 'x-b3-sampled': req.header('x-b3-sampled'), 22 | 'x-b3-flags': req.header('x-b3-flags'), 23 | 'x-ot-span-context': req.header('x-ot-span-context'), 24 | 'b3': req.header('b3') 25 | }, 26 | json: true // Automatically parses the JSON string in the response 27 | } 28 | var itemsData = [] 29 | request(request_get_options) 30 | .then(function (getresult) { 31 | // req.debug(getresult) // uncomment to show board JSON 32 | var itemsData = [] 33 | var itemListPromises = getresult.items.map(function(itemId) { return getItemRequest(req, res, user, itemId, itemsData)}); 34 | Promise.all(itemListPromises).then(function(itemsData) { 35 | res.render('board', { title: 'Cut and Paster', board: getresult, items: itemsData, errorWithItems: false }) 36 | }) 37 | .catch(function (err) { 38 | req.debug('ERROR GETTING DATA FROM BOARDS SERVICE') 39 | req.debug(err) 40 | res.render('board', { title: 'Cut and Paster', board: getresult, items: itemsData, errorWithItems: true }) 41 | }) 42 | }) 43 | .catch(function (err) { 44 | req.debug('ERROR GETTING DATA FROM BOARDS SERVICE') 45 | req.debug(err) 46 | res.render('board', { title: 'Cut and Paster', board: getresult, items: itemsData, errorWithItems: true }) 47 | }) 48 | }) 49 | 50 | function getItemRequest(req, res, user, itemId, itemsData) { 51 | const boardsGetItemURI = req.HTTP_PROTOCOL + req.BOARDS_SVC_HOST + ':' + req.BOARDS_SVC_PORT + '/' + user + '/items/' + itemId 52 | req.debug('GET from boards SVC at: ' + boardsGetItemURI) 53 | var request_getitem_options = { 54 | method: 'GET', 55 | uri: boardsGetItemURI, 56 | headers: { 57 | 'user-agent': req.header('user-agent'), 58 | 'Authorization': 'Bearer ' + res.locals.authToken, 59 | 'x-request-id': req.header('x-request-id'), 60 | 'x-b3-traceid': req.header('x-b3-traceid'), 61 | 'x-b3-spanid': req.header('x-b3-spanid'), 62 | 'x-b3-parentspanid': req.header('x-b3-parentspanid'), 63 | 'x-b3-sampled': req.header('x-b3-sampled'), 64 | 'x-b3-flags': req.header('x-b3-flags'), 65 | 'x-ot-span-context': req.header('x-ot-span-context'), 66 | 'b3': req.header('b3') 67 | }, 68 | json: true // Automatically parses the JSON string in the response 69 | } 70 | return request(request_getitem_options) 71 | .then(function (getitemresult) { 72 | // req.debug(getitemresult) // uncomment to show item JSON 73 | return getitemresult 74 | }) 75 | .catch(function (err) { 76 | req.debug('ERROR GETTING ITEM DATA FROM BOARDS SERVICE') 77 | req.debug(err) 78 | return err 79 | }) 80 | } 81 | 82 | /* POST (form submission) to add an item to board items list */ 83 | router.post('/:boardId/paste', function(req, res) { 84 | var pasteData = req.body.pastedata 85 | if (pasteData.length < 1) { 86 | req.debug('ignoring zero length add to user board') 87 | return 88 | } 89 | var user = res.locals.user 90 | var board = JSON.parse(req.body.board) 91 | const boardsNewItemURI = req.HTTP_PROTOCOL + req.BOARDS_SVC_HOST + ':' + req.BOARDS_SVC_PORT + '/' + user + '/items' 92 | req.debug('POST to boards SVC at: ' + boardsNewItemURI) 93 | var request_post_options = { 94 | method: 'POST', 95 | uri: boardsNewItemURI, 96 | body: { 97 | owner: user, 98 | type: 'string', 99 | raw: pasteData, 100 | name: '' 101 | }, 102 | headers: { 103 | 'User-Agent': req.header('user-agent'), 104 | 'Authorization': 'Bearer ' + res.locals.authToken, 105 | 'x-request-id': req.header('x-request-id'), 106 | 'x-b3-traceid': req.header('x-b3-traceid'), 107 | 'x-b3-spanid': req.header('x-b3-spanid'), 108 | 'x-b3-parentspanid': req.header('x-b3-parentspanid'), 109 | 'x-b3-sampled': req.header('x-b3-sampled'), 110 | 'x-b3-flags': req.header('x-b3-flags'), 111 | 'x-ot-span-context': req.header('x-ot-span-context'), 112 | 'b3': req.header('b3') 113 | }, 114 | json: true // Automatically parses the JSON string in the response 115 | } 116 | request(request_post_options) // ADD A NEW ITEM 117 | .then(function (postresult) { 118 | req.debug('SUCCESS CREATED NEW ITEM') 119 | req.debug(postresult) 120 | var itemId = postresult.split(' ')[1] 121 | if (board.items == null || board.items.length < 1) { 122 | board.items = [itemId] 123 | } 124 | else { 125 | board.items.push(itemId) 126 | } 127 | // req.debug(board) 128 | const boardsUpdateURI = req.HTTP_PROTOCOL + req.BOARDS_SVC_HOST + ':' + req.BOARDS_SVC_PORT + '/' + user + '/boards/' + req.params.boardId 129 | req.debug('PUT to boards SVC at: ' + boardsUpdateURI) 130 | var request_put_options = { 131 | method: 'PUT', 132 | uri: boardsUpdateURI, 133 | body: board, 134 | headers: { 135 | 'User-Agent': req.header('user-agent'), 136 | 'Authorization': 'Bearer ' + res.locals.authToken, 137 | 'x-request-id': req.header('x-request-id'), 138 | 'x-b3-traceid': req.header('x-b3-traceid'), 139 | 'x-b3-spanid': req.header('x-b3-spanid'), 140 | 'x-b3-parentspanid': req.header('x-b3-parentspanid'), 141 | 'x-b3-sampled': req.header('x-b3-sampled'), 142 | 'x-b3-flags': req.header('x-b3-flags'), 143 | 'x-ot-span-context': req.header('x-ot-span-context'), 144 | 'b3': req.header('b3') 145 | }, 146 | json: true // Automatically parses the JSON string in the response 147 | } 148 | request(request_put_options) // UPDATE BOARD TO ADD NEW ITEM IN LIST 149 | .then(function (putresult) { 150 | req.debug('SUCCESS ADDED NEW ITEM TO BOARD') 151 | req.debug(putresult) 152 | res.redirect('back') 153 | }) 154 | .catch(function (err) { 155 | req.debug('ERROR UPDATING DATA FOR BOARD') 156 | req.debug(err) 157 | res.redirect('back') 158 | }) 159 | }) 160 | .catch(function (err) { 161 | req.debug('ERROR CREATING NEW ITEM') 162 | req.debug(err) 163 | res.redirect('back') 164 | }) 165 | }) 166 | 167 | module.exports = router 168 | -------------------------------------------------------------------------------- /code/app-ui/routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var router = express.Router() 3 | var moment = require('moment') 4 | var request = require('request-promise') 5 | 6 | /* GET dashboard page. TODO: render different views based on authenticated or not */ 7 | router.get('/', function(req, res, next) { 8 | var userboards = '' 9 | var user = res.locals.user 10 | const boardsURI = req.HTTP_PROTOCOL + req.BOARDS_SVC_HOST + ':' + req.BOARDS_SVC_PORT + '/' + user + '/boards' 11 | req.debug('GET from boards SVC at: ' + boardsURI) 12 | var request_options = { 13 | method: 'GET', 14 | uri: boardsURI, 15 | headers: { 16 | 'user-agent': req.header('user-agent'), 17 | 'Authorization': 'Bearer ' + res.locals.authToken, 18 | 'x-request-id': req.header('x-request-id'), 19 | 'x-b3-traceid': req.header('x-b3-traceid'), 20 | 'x-b3-spanid': req.header('x-b3-spanid'), 21 | 'x-b3-parentspanid': req.header('x-b3-parentspanid'), 22 | 'x-b3-sampled': req.header('x-b3-sampled'), 23 | 'x-b3-flags': req.header('x-b3-flags'), 24 | 'x-ot-span-context': req.header('x-ot-span-context'), 25 | 'b3': req.header('b3') 26 | }, 27 | json: true // Automatically parses the JSON string in the response 28 | } 29 | request(request_options) 30 | .then(function (result) { 31 | userboards = result 32 | if (req.app.locals.errorAlertText != null && req.app.locals.errorAlertText != '') { // this can come from adding a new board failures too 33 | var errorAlertText = req.app.locals.errorAlertText 34 | req.app.locals.errorAlertText = null // clear the error now that we are rendering it 35 | res.render('dashboard', { title: 'Cut and Paster', user:user, boards: userboards, errorAlert: true, errorAlertText: errorAlertText}) 36 | } 37 | else 38 | res.render('dashboard', { title: 'Cut and Paster', user:user, boards: userboards, errorAlert: false }) 39 | }) 40 | .catch(function (err) { 41 | req.debug('ERROR GETTING DATA FROM BOARDS SERVICE') 42 | req.debug(err) 43 | res.render('dashboard', { title: 'Cut and Paster', user:user, boards: userboards, errorAlert: true, errorAlertText: err.toString() }) 44 | }) 45 | }) 46 | 47 | /* POST from form submission to create a board */ 48 | router.post('/newboard', function(req, res) { 49 | req.debug(req.body) 50 | var newBoardName = req.body.newboardname 51 | var newBoardDescription = req.body.newboarddesc 52 | var newBoardIsPrivate = req.body.newboardprivate 53 | var user = res.locals.user 54 | // TODO: validate data 55 | const boardsURI = req.HTTP_PROTOCOL + req.BOARDS_SVC_HOST + ':' + req.BOARDS_SVC_PORT + '/' + user + '/boards' 56 | req.debug('POST to boards SVC at: ' + boardsURI) 57 | var request_options = { 58 | method: 'POST', 59 | uri: boardsURI, 60 | body: { 61 | name: newBoardName, 62 | description: newBoardDescription, 63 | private: newBoardIsPrivate, 64 | items: [] 65 | }, 66 | headers: { 67 | 'user-agent': req.header('user-agent'), 68 | 'Authorization': 'Bearer ' + res.locals.authToken, 69 | 'x-request-id': req.header('x-request-id'), 70 | 'x-b3-traceid': req.header('x-b3-traceid'), 71 | 'x-b3-spanid': req.header('x-b3-spanid'), 72 | 'x-b3-parentspanid': req.header('x-b3-parentspanid'), 73 | 'x-b3-sampled': req.header('x-b3-sampled'), 74 | 'x-b3-flags': req.header('x-b3-flags'), 75 | 'x-ot-span-context': req.header('x-ot-span-context'), 76 | 'b3': req.header('b3') 77 | }, 78 | json: true // Automatically parses the JSON string in the response 79 | } 80 | request(request_options) 81 | .then(function (result) { 82 | //req.debug(result) 83 | req.app.locals.errorAlertText = null 84 | res.redirect('back') 85 | }) 86 | .catch(function (err) { 87 | req.debug('ERROR POSTING DATA TO CREATE NEW BOARD') 88 | req.debug(err) 89 | req.app.locals.errorAlertText = err.toString() 90 | res.redirect('back') 91 | }) 92 | }) 93 | 94 | module.exports = router 95 | -------------------------------------------------------------------------------- /code/app-ui/routes/profile.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var router = express.Router() 3 | var moment = require('moment') 4 | var request = require('request-promise') 5 | 6 | /* Show users profile page or login/create account */ 7 | router.get('/', function(req, res, next) { 8 | 9 | // TODO: if not logged in redirect to login page or jump page to allow login / registration 10 | // req.auth.checkSso() 11 | // this might change some assumptions for the service mesh workshop, need to update that 12 | // when we implement this - see redhatgov.io 13 | 14 | const user = res.locals.user 15 | const userId = res.locals.userId // this will be set if we are logged in, or fake if we are DEBUGGING 16 | getAndRender(req, res, next, userId) 17 | }) 18 | 19 | /* GET a user's page. */ 20 | router.get('/:userId', function(req, res, next) { 21 | var user = res.locals.user 22 | var userId = req.params.userId 23 | getAndRender(req, res, next, userId) 24 | }) 25 | 26 | function getAndRender(req, res, next, userId) { 27 | const profileGetURI = req.HTTP_PROTOCOL + req.PROFILE_SVC_HOST + ':' + req.PROFILE_SVC_PORT + '/users/' + userId 28 | req.debug('GET from profile SVC at: ' + profileGetURI) 29 | var request_get_options = { 30 | method: 'GET', 31 | uri: profileGetURI, 32 | headers: { 33 | 'user-agent': req.header('user-agent'), 34 | 'Authorization': 'Bearer ' + res.locals.authToken, 35 | 'x-request-id': req.header('x-request-id'), 36 | 'x-b3-traceid': req.header('x-b3-traceid'), 37 | 'x-b3-spanid': req.header('x-b3-spanid'), 38 | 'x-b3-parentspanid': req.header('x-b3-parentspanid'), 39 | 'x-b3-sampled': req.header('x-b3-sampled'), 40 | 'x-b3-flags': req.header('x-b3-flags'), 41 | 'x-ot-span-context': req.header('x-ot-span-context'), 42 | 'b3': req.header('b3') 43 | }, 44 | json: true // Automatically parses the JSON string in the response 45 | } 46 | var profileData = [] 47 | var profileImage = [] 48 | request(request_get_options) 49 | .then(function (getresult) { 50 | req.debug(getresult) // uncomment to show JSON 51 | let title = getresult.firstName + "\'s Profile" 52 | let styleId = getresult.styleId 53 | 54 | // TODO: get the profile image 55 | 56 | res.render('profile', { title: title, profile: getresult, isMyProfile: false, errorWithProfile: false, style: styleId }) 57 | }) 58 | .catch(function (err) { 59 | req.debug('ERROR GETTING DATA FROM PROFILE SERVICE') 60 | req.debug(JSON.stringify(err)) 61 | 62 | if (JSON.stringify(err).includes('ECONNREFUSED')) { 63 | res.render('profile', { title: 'Unknown User', errorWithProfile: true, errorAlert: true, errorAlertText: err.message, style: 0 }) 64 | } else { 65 | res.render('profile', { title: 'Unknown User', errorWithProfile: true, style: 0 }) 66 | } 67 | }) 68 | } 69 | 70 | module.exports = router 71 | -------------------------------------------------------------------------------- /code/app-ui/routes/search.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var router = express.Router() 3 | var moment = require('moment') 4 | var request = require('request-promise') 5 | 6 | /* POST (search form submission) */ 7 | router.post('/', function(req, res) { 8 | var searchTerm = req.body.term 9 | req.debug('searching for ' + searchTerm) 10 | if (searchTerm===null || searchTerm.length < 1) { 11 | req.debug('ignoring zero length searchTerm') 12 | res.redirect('/') 13 | } 14 | res.render('search', { title: 'Search Results', searchTerm: searchTerm }) 15 | 16 | // TODO: build the search service and call it 17 | 18 | // const searchURI = req.HTTP_PROTOCOL + req.SEARCH_SVC_HOST + ':' + req.SEARCH_SVC_PORT + '/search' 19 | // req.debug('POST to boards SVC at: ' + boardsURI) 20 | // var request_options = { 21 | // method: 'GET', 22 | // uri: searchURI, 23 | // body: { 24 | // owner: res.locals.user, 25 | // type: 'string', 26 | // raw: searchTerm, 27 | // name: '' 28 | // }, 29 | // headers: { 30 | // 'User-Agent': req.header('user-agent'), 31 | // 'Authorization': 'Bearer ' + res.locals.authToken, 32 | // 'x-request-id': req.header('x-request-id'), 33 | // 'x-b3-traceid': req.header('x-b3-traceid'), 34 | // 'x-b3-spanid': req.header('x-b3-spanid'), 35 | // 'x-b3-parentspanid': req.header('x-b3-parentspanid'), 36 | // 'x-b3-sampled': req.header('x-b3-sampled'), 37 | // 'x-b3-flags': req.header('x-b3-flags'), 38 | // 'x-ot-span-context': req.header('x-ot-span-context'), 39 | // 'b3': req.header('b3') 40 | // }, 41 | // json: true // Automatically parses the JSON string in the response 42 | // } 43 | // request(request_options) 44 | // .then(function (result) { 45 | // req.debug(result) 46 | // res.redirect('back') 47 | // }) 48 | // .catch(function (err) { 49 | // req.debug('ERROR COMMUNICATING WITH TO SEARCH SERVICE') 50 | // req.debug(err) 51 | // res.redirect('back') 52 | // }) 53 | }) 54 | 55 | module.exports = router 56 | -------------------------------------------------------------------------------- /code/app-ui/routes/shared.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var router = express.Router() 3 | var moment = require('moment') 4 | var request = require('request-promise') 5 | 6 | /* GET shared page. */ 7 | router.get('/', function(req, res, next) { 8 | const boardsURI = req.HTTP_PROTOCOL + req.BOARDS_SVC_HOST + ':' + req.BOARDS_SVC_PORT + '/shareditems' 9 | req.debug('GET from boards SVC at: ' + boardsURI) 10 | var request_options = { 11 | method: 'GET', 12 | uri: boardsURI, 13 | headers: { 14 | 'user-agent': req.header('user-agent'), 15 | 'Authorization': 'Bearer ' + res.locals.authToken, 16 | 'x-request-id': req.header('x-request-id'), 17 | 'x-b3-traceid': req.header('x-b3-traceid'), 18 | 'x-b3-spanid': req.header('x-b3-spanid'), 19 | 'x-b3-parentspanid': req.header('x-b3-parentspanid'), 20 | 'x-b3-sampled': req.header('x-b3-sampled'), 21 | 'x-b3-flags': req.header('x-b3-flags'), 22 | 'x-ot-span-context': req.header('x-ot-span-context'), 23 | 'b3': req.header('b3') 24 | }, 25 | json: true // Automatically parses the JSON string in the response 26 | } 27 | request(request_options) 28 | .then(function (result) { 29 | // req.debug('GOT SHARED ITEMS:') 30 | // req.debug(result) 31 | res.render('shared', { title: 'Cut and Paster', board: {name: 'Shared Items'}, items: result, errorWithItems: false }) 32 | }) 33 | .catch(function (err) { 34 | req.debug('ERROR GETTING DATA FROM BOARDS SERVICE') 35 | req.debug(err) 36 | res.render('shared', { title: 'Cut and Paster', board: {name: 'Shared Items'}, items: [], errorWithItems: true, errorAlert: true, errorAlertText: err.toString() }) 37 | }) 38 | }) 39 | 40 | /* POST (form submission) to add an item to shared items list */ 41 | router.post('/paste', function(req, res) { 42 | var pasteData = req.body.pastedata 43 | if (pasteData.length < 1) { 44 | req.debug('ignoring zero length add to shared board') 45 | return 46 | } 47 | const boardsURI = req.HTTP_PROTOCOL + req.BOARDS_SVC_HOST + ':' + req.BOARDS_SVC_PORT + '/shareditems' 48 | req.debug('POST to boards SVC at: ' + boardsURI) 49 | var request_options = { 50 | method: 'POST', 51 | uri: boardsURI, 52 | body: { 53 | owner: res.locals.user, 54 | type: 'string', 55 | raw: pasteData, 56 | name: '' 57 | }, 58 | headers: { 59 | 'User-Agent': req.header('user-agent'), 60 | 'Authorization': 'Bearer ' + res.locals.authToken, 61 | 'x-request-id': req.header('x-request-id'), 62 | 'x-b3-traceid': req.header('x-b3-traceid'), 63 | 'x-b3-spanid': req.header('x-b3-spanid'), 64 | 'x-b3-parentspanid': req.header('x-b3-parentspanid'), 65 | 'x-b3-sampled': req.header('x-b3-sampled'), 66 | 'x-b3-flags': req.header('x-b3-flags'), 67 | 'x-ot-span-context': req.header('x-ot-span-context'), 68 | 'b3': req.header('b3') 69 | }, 70 | json: true // Automatically parses the JSON string in the response 71 | } 72 | request(request_options) 73 | .then(function (result) { 74 | // req.debug(result) 75 | res.redirect('/shared') 76 | }) 77 | .catch(function (err) { 78 | req.debug('ERROR POSTING DATA TO BOARDS SERVICE') 79 | req.debug(err) 80 | res.render('shared', { title: 'Cut and Paster', board: {name: 'Shared Items'}, items: [], errorWithItems: true, errorAlert: true, errorAlertText: err.toString() }) 81 | }) 82 | }) 83 | 84 | module.exports = router 85 | -------------------------------------------------------------------------------- /code/app-ui/views/board.pug: -------------------------------------------------------------------------------- 1 | extends layout-items 2 | 3 | block append content 4 | br 5 | main.container(role='main') 6 | .container 7 | .justify-content-md-center-2.shadow-sm 8 | form(type='form' action='/'+user+'/board/'+board.id+'/paste' method='post') 9 | div.input-group.input-group-lg 10 | input#inputPasteData( 11 | name='pastedata' 12 | type='text' 13 | class='form-control' 14 | placeholder='Paste your text here...' 15 | autofocus 16 | ) 17 | input#inputBoardData(type='hidden', name='board', value=board) 18 | span.input-group-btn.pl-2 19 | button.btn.btn-default.btn-dark.btn-lg(type='submit') Add 20 | hr 21 | .d-flex.align-items-center.p-3.my-3.text-white-50.bg-brown.rounded.shadow-sm 22 | img.mr-3(src='/images/folder.png', alt='', width='48', height='48') 23 | .lh-100 24 | h6.mb-0.text-white.lh-100 #{board.name} 25 | small #{board.description} 26 | .my-3.p-3.bg-white.rounded.shadow-sm 27 | if errorWithItems == true 28 | .alert.alert-danger.alert-dismissible.fade.show(role='alert') 29 | strong Error with getting the items! 30 | | You should try refreshing the page to see if that fixes this 31 | button.close(type='button', data-dismiss='alert', aria-label='Close') 32 | span(aria-hidden='true') × 33 | if !errorWithItems && (items == null || items.length < 0) 34 | //- Empty items message could go here 35 | else 36 | - var count = Math.min(150, items.length) // TODO: limit the max displayed items, pagination? scroll loader? 37 | - var iter = 0 38 | while iter < count 39 | - var owner = 'unknown' 40 | if items[iter].owner != null 41 | - owner = items[iter].owner 42 | - var itemText = '' 43 | if items[iter].raw != null 44 | - itemText = items[iter].raw 45 | .media.text-muted.pt-3.border-bottom.border-gray 46 | p.media-body.pb-3.mb-0.small.lh-125 47 | strong.d-block.text-gray-dark #{owner} 48 | | #{itemText} 49 | p 50 | button.btn.btn-outline-secondary.btn-sm(data-clipboard-text=''+itemText) Copy Me 51 | - iter++ 52 | small.d-block.text-right.mt-3 53 | a(href='#') All recent 54 | //- enable to copy paste using the clipboardjs lib 55 | script. 56 | var btns = document.querySelectorAll('button'); 57 | var clipboard = new ClipboardJS(btns); 58 | clipboard.on('success', function(e) { 59 | console.log(e); 60 | }); 61 | clipboard.on('error', function(e) { 62 | console.log(e); 63 | }); 64 | 65 | block append javascripts 66 | script. 67 | var btns = document.querySelectorAll('button'); 68 | var clipboard = new ClipboardJS(btns); 69 | clipboard.on('success', function(e) { 70 | console.log(e); 71 | }); 72 | clipboard.on('error', function(e) { 73 | console.log(e); 74 | }); -------------------------------------------------------------------------------- /code/app-ui/views/dashboard.pug: -------------------------------------------------------------------------------- 1 | extends layout-topbar 2 | block append stylesheets 3 | link(href='/stylesheets/dashboard.css', rel='stylesheet') 4 | block content 5 | if errorAlert == true 6 | .alert.alert-danger.alert-dismissible.fade.show(role='alert') 7 | strong Yikes! Something went wrong... Please try again. 8 | br 9 | | The details: #{errorAlertText} 10 | button.close(type='button', data-dismiss='alert', aria-label='Close') 11 | span(aria-hidden='true') × 12 | main(role='main') 13 | section.jumbotron.text-center 14 | .container 15 | if boards != null && boards.length != 0 16 | if locals.authenticated == false 17 | h1.jumbotron-heading Public Boards 18 | p.lead.text-muted 19 | | Public and anonymous boards are below, click on one to see the items. 20 | br 21 | | You can also create more boards using the button below. 22 | else 23 | h1.jumbotron-heading Your Boards 24 | p.lead.text-muted 25 | | Your boards are below, click on one to see the items. 26 | br 27 | | You can also create more boards using the button below. 28 | else 29 | h1.jumbotron-heading Look at all the empty 30 | p.lead.text-muted 31 | | You can create a new board by clicking the button below. Once you've created a board, you can start to paste items into it. 32 | p 33 | button.btn.btn-primary(type='button', data-toggle='modal', data-target='#newBoardModal') New Board 34 | #newBoardModal.modal.fade(tabindex='-1', role='dialog', aria-labelledby='newBoardModalLabel', aria-hidden='true') 35 | .modal-dialog(role='document') 36 | .modal-content 37 | form(type='form' action='/newboard' method='post')#newBoardForm 38 | .modal-header 39 | h5#newBoardModalLabel.modal-title New Board 40 | button.close(type='button', data-dismiss='modal', aria-label='Close') 41 | span(aria-hidden='true') x 42 | .modal-body 43 | .form-group.row 44 | label.col-form-label.col-sm-3(for='newboardName') Name 45 | div.col-sm-9 46 | input#newboardName.form-control(name='newboardname' type='text', placeholder='Like \"Linux Commands\" or \"Good Quotes\"') 47 | .form-group.row 48 | label.col-form-label.col-sm-3(for='newboardDescription') Description 49 | div.col-sm-9 50 | textarea#newboardDescription.form-control(name='newboarddesc' placeholder='Describe what this board is all about') 51 | .form-group.row 52 | label.col-form-label.col-sm-3(for='newboardPrivate') Make Private 53 | div.col-sm-9 54 | input#newboardPrivate.form-control(name='newboardprivate' type="checkbox" data-toggle="tooltip" data-placement="bottom" title="Keep this board secret") 55 | //- TODO COLOR PICKER AND ICON IMAGE 56 | .modal-footer 57 | .form-group.row 58 | div.col-sm-5 59 | button.btn.btn-secondary(type='button', data-dismiss='modal') Cancel 60 | div.col-sm-5 61 | button#newboardSubmitButton.btn.btn-primary(type='submit') Create 62 | if boards != null && boards.length != 0 63 | .album.py-5.bg-light 64 | .container 65 | .row 66 | - var count = Math.min(50, boards.length) // limit the max displayed boards 67 | - var iter = 0 68 | while iter < count 69 | if boards[iter].id != null && boards[iter].id.length > 0 70 | .col-md-4 71 | .card.mb-4.shadow-sm 72 | svg.bd-placeholder-img.card-img-top(width='100%', height='100', xmlns='http://www.w3.org/2000/svg', preserveAspectRatio='xMidYMid slice', focusable='false', role='img', aria-label='Placeholder: Header') 73 | title Header 74 | rect(width='100%', height='100%', fill='#55595c') 75 | text(x='50%', y='50%', fill='#eceeef', dy='.3em') #{boards[iter].name} 76 | .card-body 77 | p.card-text 78 | | #{boards[iter].description} 79 | .d-flex.justify-content-between.align-items-center 80 | .btn-group 81 | a.button.btn.btn-sm.btn-outline-secondary(href='/'+user+'/board/'+boards[iter].id, type='button') Click to Open 82 | //- button.btn.btn-sm.btn-outline-secondary.btn-warning(type='button') Delete 83 | small.text-muted #{boards[iter].items.length} items 84 | else 85 | .col-md-4 86 | .card.mb-4.shadow-sm 87 | svg.bd-placeholder-img.card-img-top(width='100%', height='100', xmlns='http://www.w3.org/2000/svg', preserveAspectRatio='xMidYMid slice', focusable='false', role='img', aria-label='Placeholder: Header') 88 | title Header 89 | rect(width='100%', height='100%', fill='#55595c') 90 | text(x='50%', y='50%', fill='#eceeef', dy='.3em') ????????? 91 | .card-body 92 | p.card-text 93 | | Something is wrong with this board, contact and admin if you would like to try to recover it. 94 | .d-flex.justify-content-between.align-items-center 95 | .btn-group 96 | button.btn.btn-sm.btn-outline-secondary(type='button') Cannot Open 97 | //- button.btn.btn-sm.btn-outline-secondary.btn-warning(type='button') Delete 98 | small.text-muted ? items 99 | - iter++ 100 | 101 | block append javascripts 102 | script(src='/javascripts/dashboard.js') -------------------------------------------------------------------------------- /code/app-ui/views/error.pug: -------------------------------------------------------------------------------- 1 | extends layout-topbar 2 | block append stylesheets 3 | link(href='/stylesheets/dashboard.css', rel='stylesheet') 4 | block content 5 | h1= message 6 | h2= error.status 7 | pre #{error.stack} 8 | -------------------------------------------------------------------------------- /code/app-ui/views/info-page.pug: -------------------------------------------------------------------------------- 1 | extends layout-topbar 2 | block append stylesheets 3 | link(href='/stylesheets/dashboard.css', rel='stylesheet') 4 | block content 5 | if infoAlert == true 6 | .alert.alert-info.alert-dismissible.fade.show(role='alert') 7 | strong Hey, read this info 8 | br 9 | | #{infoAlertText} 10 | button.close(type='button', data-dismiss='alert', aria-label='Close') 11 | span(aria-hidden='true') × 12 | main(role='main') 13 | section.jumbotron.text-center 14 | .container 15 | h3.jumbotron-heading INFO: 16 | p.lead.text-muted 17 | | #{infoMessage} 18 | br 19 | p.info-text #{infoDetails} 20 | -------------------------------------------------------------------------------- /code/app-ui/views/layout-items.pug: -------------------------------------------------------------------------------- 1 | extends layout-topbar 2 | block secondbar 3 | .nav-scroller.bg-white.shadow-sm 4 | nav.nav.nav-underline 5 | a.nav-link(href='#').disabled You're looking at 6 | a.nav-link(href='#').active 7 | | #{board.name} 8 | //span.badge.badge-pill.bg-light.align-text-bottom 0 9 | //a.nav-link(href='#').disabled Just Exploring (coming soon) 10 | //a.nav-link(href='#').disabled Similar to Mine (coming soon) 11 | block content 12 | if errorAlert == true 13 | .alert.alert-danger.alert-dismissible.fade.show(role='alert') 14 | strong Yikes! Something went wrong... Please try again. 15 | br 16 | | The details: #{errorAlertText} 17 | button.close(type='button', data-dismiss='alert', aria-label='Close') 18 | span(aria-hidden='true') × 19 | // A modal error popup that we can rig via JS/JQuery events 20 | #errorModal.modal.fade 21 | .modal-dialog.modal-confirm 22 | .modal-content 23 | .modal-header 24 | .icon-box 25 | i.material-icons  26 | button.close(type='button', data-dismiss='modal', aria-hidden='true') × 27 | .modal-body.text-center 28 | h4 Ooops! 29 | p Something went wrong loading the items. Please refresh to try again. 30 | button.btn.btn-success(data-dismiss='modal') Try Again 31 | block append javascripts 32 | script(src='/javascripts/clipboard.min.js') -------------------------------------------------------------------------------- /code/app-ui/views/layout-topbar.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | meta(charset='utf-8') 5 | meta(name='viewport', content='width=device-width, initial-scale=1, shrink-to-fit=no') 6 | title Paste Board 7 | link(href='/stylesheets/bootstrap-4.3.1-dist/css/bootstrap.min.css', rel='stylesheet') 8 | style. 9 | .bd-placeholder-img { 10 | font-size: 1.125rem; 11 | text-anchor: middle; 12 | -webkit-user-select: none; 13 | -moz-user-select: none; 14 | -ms-user-select: none; 15 | user-select: none; 16 | } 17 | @media (min-width: 768px) { 18 | .bd-placeholder-img-lg { 19 | font-size: 3.5rem; 20 | } 21 | } 22 | block stylesheets 23 | link(href='/stylesheets/topbar.css', rel='stylesheet') 24 | body.bg-light 25 | nav.navbar.navbar-expand-lg.fixed-top.navbar-dark.bg-dark.flex-wrap2.flex-md-nowrap 26 | img.mr-3(src='/images/clipboard.png', alt='', width='50', height='50') 27 | button.navbar-toggler.p-0.border-0(type='button', data-toggle='offcanvas') 28 | span.navbar-toggler-icon 29 | .navbar-collapse.offcanvas-collapse(id='navbarContent') 30 | ul.navbar-nav.flex-grow-1 31 | li.nav-item 32 | a.nav-link(href='/') 33 | | Dashboard 34 | span.sr-only (current) 35 | li.nav-item 36 | a.nav-link(href='/shared') Shared 37 | li.nav-item 38 | a.nav-link(href='/profile') Profile 39 | form.flex-grow-1.mx-5.my-2.my-lg-0(type='form' action='/search' method='post') 40 | .input-group.py-1.px-2.px-md-0 41 | input.form-control.form-control-dark(name='term' type='text' placeholder='Search here...' aria-label='Search') 42 | .input-group-append 43 | button.btn.btn-outline-light(type='submit') 44 | i.fa.fa-search Search 45 | //- login button and form 46 | ul.navbar-nav.navbar-right 47 | li.nav-item.dropdown 48 | a#dropdown-notifications.nav-link.dropdown-toggle(href='#', data-toggle='dropdown', aria-haspopup='true', aria-expanded='false') Notifications 49 | .dropdown-menu(aria-labelledby='dropdown-notifications') 50 | a.dropdown-item(href='#').disabled No new notifications 51 | li.nav-item.dropdown 52 | if locals.authenticated == false 53 | a#dropdown-login.nav-link.dropdown-toggle(href='#', data-toggle='dropdown', aria-haspopup='true', aria-expanded='false') Login 54 | else 55 | a#dropdown-login.nav-link.dropdown-toggle(href='#', data-toggle='dropdown', aria-haspopup='true', aria-expanded='false') #{locals.username} 56 | ul#login-dp.dropdown-menu.dropdown-menu-right(aria-labelledby='dropdown-login') 57 | li 58 | .row 59 | .col-md-12 60 | if locals.authenticated == false 61 | a.dropdown-item(href='#').disabled You are anonymous right now 62 | a.btn.btn-primary.btn-block.my-3(href='/login', role='button') Login 63 | else 64 | a.dropdown-item(href='#').disabled #{locals.username} 65 | a.btn.btn-primary.btn-block.my-3(href='/logout', role='button') Logout 66 | br 67 | block secondbar 68 | 69 | block content 70 | 71 | footer.text-muted 72 | .container 73 | p.float-right 74 | a(href='#') Back to top 75 | p.badge.badge-light 76 | br 77 | br 78 | | The source for this application can be 79 | a(href='https://github.com/dudash/openshift-microservices') found on github 80 | //- a(href='https://vecteezy.com').badge.badge-light - graphics provided by www.Vecteezy.com 81 | br 82 | br 83 | p(align='center') 84 | | Made with ❤️ at 85 | img(src='/images/Logo-RedHat-A-Color-RGB.png' width='100') 86 | 87 | block javascripts 88 | script(src='https://code.jquery.com/jquery-3.3.1.slim.min.js', integrity='sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo', crossorigin='anonymous') 89 | script. 90 | window.jQuery || document.write('