├── .gitignore ├── README.md ├── backend ├── .gitignore ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── example │ │ └── demo │ │ └── shop │ │ ├── ShopApplication.java │ │ ├── controller │ │ ├── CustomerController.java │ │ ├── OrderController.java │ │ ├── ProductCatalogController.java │ │ └── ProductController.java │ │ ├── exceptions │ │ └── DoesNotExistException.java │ │ ├── model │ │ ├── BaseEntity.java │ │ ├── CustomerInfo.java │ │ ├── LocalDateAttributeConverter.java │ │ ├── Order.java │ │ ├── Product.java │ │ └── ProductCatalog.java │ │ ├── repository │ │ ├── OrderRepository.java │ │ ├── ProductCatalogRepository.java │ │ └── ProductRepository.java │ │ └── service │ │ ├── CustomerServiceImpl.java │ │ ├── OrderServiceImpl.java │ │ ├── ProductCatalogServiceImpl.java │ │ └── ProductServiceImpl.java │ └── resources │ ├── application.properties │ ├── data.sql │ └── schema.sql ├── frontend ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.e2e.json ├── package.json ├── proxy.conf.json ├── src │ ├── app │ │ ├── app-auth.guard.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── components │ │ │ ├── alert │ │ │ │ ├── alert.component.html │ │ │ │ └── alert.component.ts │ │ │ ├── cart │ │ │ │ ├── cart.component.html │ │ │ │ └── cart.component.ts │ │ │ ├── customer-orders │ │ │ │ ├── customer-orders.component.html │ │ │ │ └── customer-orders.component.ts │ │ │ ├── customers │ │ │ │ ├── customers.component.html │ │ │ │ └── customers.component.ts │ │ │ ├── orders │ │ │ │ ├── orders.component.html │ │ │ │ └── orders.component.ts │ │ │ ├── product-catalog │ │ │ │ ├── product-catalog.component.html │ │ │ │ └── product-catalog.component.ts │ │ │ └── product │ │ │ │ ├── product.component.html │ │ │ │ └── product.component.ts │ │ ├── directives │ │ │ └── two-digit-decimal-number.directive.ts │ │ ├── interceptor │ │ │ └── http-error.interceptor.ts │ │ ├── models │ │ │ ├── customer.info.ts │ │ │ ├── order.ts │ │ │ ├── product-catalog.ts │ │ │ └── product.ts │ │ ├── services │ │ │ ├── alert.service.ts │ │ │ ├── customer.service.ts │ │ │ ├── order.service.ts │ │ │ ├── product-catalog.service.ts │ │ │ └── product.service.ts │ │ └── stores │ │ │ ├── customer.store.ts │ │ │ └── store.ts │ ├── assets │ │ └── .gitkeep │ ├── browserslist │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── karma.conf.js │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ ├── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tsconfig.json ├── tslint.json └── yarn.lock ├── images ├── keycloak_login.png ├── shop.ico └── shopapp.png ├── keycloak └── demo-realm.json └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | .settings 3 | .classpath 4 | .project -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grocery Shop 2 | 3 | ![Grocery Shop](images/shop.ico?raw=true "Grocery Shop") 4 | 5 | This is the source code for my article on [how to securing a Angular frontend and a Spring Boot backend using Keycloak](https://www.linkedin.com/pulse/securing-java-rest-services-keycloak-part-4-jannie-louwrens/). 6 | 7 | ## Requirements 8 | 9 | - Keycloak 4.6.0.Final 10 | - Java 8 11 | - Spring Boot 2.0.7.RELEASE 12 | - Angular 7.1.4 13 | - Node.js 10.15.0 14 | - Yarn 1.12.3 15 | 16 | ## Installing and Configuring Keycloak 17 | Download the standalone server distribution from the [Keycloak website](https://www.keycloak.org/), unpack it and start the server. Follow the [Getting Started](https://www.keycloak.org/docs/latest/getting_started/index.html#creating-the-admin-account) instructions to setup the administrator account. 18 | 19 | There are two ways to configure the Keycloak realm for this application: 20 | 1. Import the [demo-realm.json](keycloak/demo-realm.json) 21 | 2. Follow the **Create Realm, Client and Users** guide 22 | 23 | ### Create Realm, Client and Users 24 | >This section is only for those who wish to manually configure the Keycloak server. 25 | 26 | #### 1. Create a realm 27 | Follow the [create a realm](https://www.keycloak.org/docs/latest/getting_started/index.html#_create-realm) instructions and create a realm called: `demo` 28 | #### 2. Create a client 29 | Follow steps 1- 3 of the [creating and registering](https://www.keycloak.org/docs/latest/getting_started/index.html#creating-and-registering-the-client) guide and create a new client called: `my-app` 30 | 31 | In the **Valid Redirect URIs** field enter the two URLs: `http://localhost:8081/*` and `http://localhost:4200/*` 32 | > Note the asterisk (*) after the urls! 33 | 34 | And in the **Web Origins** fields simply add a `*` (asterisk) 35 | #### 3. Create roles and assign permissions 36 | In the Keycloak administration console create two new roles, named: `user` and `admin` 37 | Edit the `admin` role and enable the **Composite Roles** flag and choose `realm-management` from the **Client Roles** droplist. 38 | Highlight the `view-users` option in the **Available Roles** block and then click on the "Add selected" button. 39 | #### 4. Create the following users: 40 | | Username | Password | First Name | Last Name | Email | Roles | 41 | | ------ | ------ | ------ | ------ | ------ | ------ | 42 | | metalgear | password | Bob | Knight | bob.knight@example.com | ADMIN, USER | 43 | | grilldad | password | Jim | Long | jim.long@example.com | USER | 44 | | mythbuster | password | Kate | Wilson | kate.wilson@example.com | USER | 45 | | spacehunter | password | Victor | Brown | victor.brown@example.com | USER | 46 | > It is most important that you enter the username as provided in the table, because they are used in the Spring backend to link the customer orders with the user. 47 | 48 | ## Start the Spring Boot Application 49 | Open a terminal and change to the directory where the code was checked out. 50 | Next change to the `backend` directory and execure the following maven command: 51 | ``` 52 | mvn clean package spring-boot:run 53 | ``` 54 | ## Build and Run the Angular Frontend Application 55 | Open another terminal and change to the directory where the code was checked out. 56 | Next change to the `frontend` directory abd execute the following commands: 57 | ``` 58 | yarn install 59 | yarn start 60 | ``` 61 | ## View the Application 62 | Open an internet browser and navigate to the url: `http://localhost:4200` 63 | 64 | You will be presented with the Keycloak login screen: 65 | 66 | ![Keycloak Login](images/keycloak_login.png?raw=true "Keycloak Login") 67 | 68 | Enter one of the username and password combination created earlier to sign in. 69 | 70 | After a successful login you will see the product catalog page: 71 | ![Landing Page](images/shopapp.png?raw=true "Landing Page") 72 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | .settings 3 | .classpath 4 | .project -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # spring-boot-keycloak-angular 2 | Securing a Angular frontend and a Spring Boot backend with Keycloak and Spring Security 3 | -------------------------------------------------------------------------------- /backend/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example.keycloak.demo 7 | spring-boot-keycloak-backend 8 | 1.0.0 9 | jar 10 | 11 | spring-boot-keycloak-backend 12 | Spring Boot project for the Spring Boot and Angular and Keycloak security demo 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.7.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 25 | 1.8 26 | 4.6.0.Final 27 | 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-data-jpa 40 | 41 | 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-devtools 46 | runtime 47 | 48 | 49 | 51 | 52 | org.keycloak 53 | keycloak-spring-boot-starter 54 | 55 | 56 | 57 | 58 | com.h2database 59 | h2 60 | runtime 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.keycloak.bom 69 | keycloak-adapter-bom 70 | ${keycloak.version} 71 | pom 72 | import 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | org.springframework.boot 82 | spring-boot-maven-plugin 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/ShopApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ShopApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ShopApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/controller/CustomerController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.controller; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import org.keycloak.KeycloakPrincipal; 7 | import org.keycloak.KeycloakSecurityContext; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import com.example.demo.shop.exceptions.DoesNotExistException; 16 | import com.example.demo.shop.model.CustomerInfo; 17 | import com.example.demo.shop.service.CustomerServiceImpl; 18 | 19 | @RestController 20 | @RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_VALUE) 21 | public class CustomerController { 22 | 23 | @Autowired 24 | private CustomerServiceImpl customerService; 25 | 26 | @GetMapping("customers") 27 | public List searchForCustomers(@RequestParam("username") Optional username, 28 | KeycloakPrincipal principal) throws DoesNotExistException { 29 | if (username.isPresent()) { 30 | return customerService.getCustomerByUsername(username.get(), principal); 31 | } 32 | 33 | return customerService.getCustomers(principal); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/controller/OrderController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.controller; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import javax.validation.Valid; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.PostMapping; 13 | import org.springframework.web.bind.annotation.PutMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RequestParam; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | import com.example.demo.shop.exceptions.DoesNotExistException; 20 | import com.example.demo.shop.model.Order; 21 | import com.example.demo.shop.service.OrderServiceImpl; 22 | 23 | @RestController 24 | @RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_VALUE) 25 | public class OrderController { 26 | 27 | @Autowired 28 | private OrderServiceImpl orderService; 29 | 30 | @GetMapping("/orders/{id}") 31 | public Order getOrder(@PathVariable String id) throws DoesNotExistException { 32 | return orderService.getOrder(id); 33 | } 34 | 35 | @GetMapping("/orders") 36 | public List searchForOrders(@RequestParam("customerId") Optional customerId) { 37 | if (customerId.isPresent()) { 38 | return orderService.getOrdersByCustomer(customerId.get()); 39 | } 40 | return orderService.getOrders(); 41 | } 42 | 43 | @PostMapping("/orders") 44 | public Order createOrder(@RequestParam("customerId") String customerId, @Valid @RequestBody Order order) { 45 | return orderService.createOrder(customerId, order); 46 | } 47 | 48 | @PutMapping("/orders/{id}") 49 | public Order updateOrder(@PathVariable String id, @Valid @RequestBody Order order) throws DoesNotExistException { 50 | return orderService.updateOrder(id, order); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/controller/ProductCatalogController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.controller; 2 | 3 | import java.util.List; 4 | 5 | import javax.validation.Valid; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.PutMapping; 13 | import org.springframework.web.bind.annotation.RequestBody; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | import com.example.demo.shop.exceptions.DoesNotExistException; 18 | import com.example.demo.shop.model.ProductCatalog; 19 | import com.example.demo.shop.service.ProductCatalogServiceImpl; 20 | 21 | @RestController 22 | @RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_VALUE) 23 | public class ProductCatalogController { 24 | 25 | @Autowired 26 | private ProductCatalogServiceImpl productCatalogService; 27 | 28 | @GetMapping("/productcatalogs/{id}") 29 | public ProductCatalog getProductCatalog(@PathVariable String id) throws DoesNotExistException { 30 | return productCatalogService.getProductCatalog(id); 31 | } 32 | 33 | @GetMapping("/productcatalogs") 34 | public List getProductCatalogs() { 35 | return productCatalogService.getProductCatalogs(); 36 | } 37 | 38 | @PostMapping("/productcatalogs") 39 | public ProductCatalog createProductCatalog(@Valid @RequestBody ProductCatalog productCatalog) { 40 | return productCatalogService.createProductCatalog(productCatalog); 41 | } 42 | 43 | @PutMapping("/productcatalogs/{id}") 44 | public ProductCatalog updateProductCatalog(@PathVariable String id, 45 | @Valid @RequestBody ProductCatalog productCatalog) throws DoesNotExistException { 46 | return productCatalogService.updateProductCatalog(id, productCatalog); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/controller/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.controller; 2 | 3 | import java.time.LocalDate; 4 | import java.util.List; 5 | import java.util.Optional; 6 | 7 | import javax.validation.Valid; 8 | 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.format.annotation.DateTimeFormat; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.PutMapping; 16 | import org.springframework.web.bind.annotation.RequestBody; 17 | import org.springframework.web.bind.annotation.RequestMapping; 18 | import org.springframework.web.bind.annotation.RequestParam; 19 | import org.springframework.web.bind.annotation.RestController; 20 | 21 | import com.example.demo.shop.exceptions.DoesNotExistException; 22 | import com.example.demo.shop.model.Product; 23 | import com.example.demo.shop.service.ProductServiceImpl; 24 | 25 | @RestController 26 | @RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_VALUE) 27 | public class ProductController { 28 | 29 | @Autowired 30 | private ProductServiceImpl productService; 31 | 32 | @GetMapping("/products/{id}") 33 | public Product getProduct(@PathVariable String id) throws DoesNotExistException { 34 | return productService.getProduct(id); 35 | } 36 | 37 | @GetMapping("/products") 38 | public List searchForProducts(@RequestParam("productCatalogId") Optional productCatalogId, 39 | @RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Optional date) { 40 | if (productCatalogId.isPresent() && date.isPresent()) { 41 | return productService.getEffectiveProductsByProductCatalogOnDate(productCatalogId.get(), date.get()); 42 | } 43 | if (productCatalogId.isPresent()) { 44 | return productService.getProductsByProductCatalog(productCatalogId.get()); 45 | } 46 | if (date.isPresent()) { 47 | return productService.getEffectiveProductsOnDate(date.get()); 48 | } 49 | return productService.getProducts(); 50 | } 51 | 52 | @PostMapping("/products") 53 | public Product createProduct(@RequestParam("productCatalogId") String productCatalogId, 54 | @Valid @RequestBody Product product) { 55 | return productService.createProduct(productCatalogId, product); 56 | } 57 | 58 | @PutMapping("/products/{id}") 59 | public Product updateProduct(@PathVariable String id, @Valid @RequestBody Product product) 60 | throws DoesNotExistException { 61 | return productService.updateProduct(id, product); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/exceptions/DoesNotExistException.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class DoesNotExistException extends Exception { 8 | 9 | private static final long serialVersionUID = 1L; 10 | 11 | public DoesNotExistException() { 12 | } 13 | 14 | public DoesNotExistException(String message) { 15 | super(message); 16 | } 17 | 18 | public DoesNotExistException(String message, Throwable cause) { 19 | super(message, cause); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/model/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.UUID; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.Id; 8 | import javax.persistence.MappedSuperclass; 9 | import javax.persistence.PrePersist; 10 | 11 | @MappedSuperclass 12 | public class BaseEntity implements Serializable { 13 | 14 | private static final long serialVersionUID = 1L; 15 | 16 | @Id 17 | @Column(unique = true, nullable = false) 18 | private String id; 19 | 20 | @PrePersist 21 | public void prePersist() { 22 | if (this.id == null) { 23 | this.id = UUID.randomUUID().toString(); 24 | } 25 | } 26 | 27 | public String getId() { 28 | return id; 29 | } 30 | 31 | public void setId(String id) { 32 | this.id = id; 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | final int prime = 31; 38 | int hash = 17; 39 | hash = hash * prime + ((id == null) ? 0 : id.hashCode()); 40 | return hash; 41 | } 42 | 43 | @Override 44 | public boolean equals(Object other) { 45 | if (this == other) { 46 | return true; 47 | } 48 | if (!(other instanceof BaseEntity)) { 49 | return false; 50 | } 51 | BaseEntity castOther = (BaseEntity) other; 52 | return this.id.equals(castOther.id); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/model/CustomerInfo.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.model; 2 | 3 | import java.io.Serializable; 4 | 5 | public class CustomerInfo implements Serializable { 6 | 7 | private static final long serialVersionUID = 1L; 8 | 9 | private String id; 10 | 11 | private String firstName; 12 | 13 | private String lastName; 14 | 15 | private String username; 16 | 17 | private String email; 18 | 19 | public CustomerInfo() { 20 | } 21 | 22 | public String getId() { 23 | return id; 24 | } 25 | 26 | public void setId(String id) { 27 | this.id = id; 28 | } 29 | 30 | public String getFirstName() { 31 | return firstName; 32 | } 33 | 34 | public void setFirstName(String firstName) { 35 | this.firstName = firstName; 36 | } 37 | 38 | public String getLastName() { 39 | return lastName; 40 | } 41 | 42 | public void setLastName(String lastName) { 43 | this.lastName = lastName; 44 | } 45 | 46 | public String getUsername() { 47 | return username; 48 | } 49 | 50 | public void setUsername(String username) { 51 | this.username = username; 52 | } 53 | 54 | public String getEmail() { 55 | return email; 56 | } 57 | 58 | public void setEmail(String email) { 59 | this.email = email; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/model/LocalDateAttributeConverter.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.model; 2 | 3 | import java.sql.Date; 4 | import java.time.LocalDate; 5 | 6 | import javax.persistence.AttributeConverter; 7 | import javax.persistence.Converter; 8 | 9 | @Converter(autoApply = true) 10 | public class LocalDateAttributeConverter implements AttributeConverter { 11 | 12 | @Override 13 | public Date convertToDatabaseColumn(LocalDate localDate) { 14 | return (localDate == null ? null : Date.valueOf(localDate)); 15 | } 16 | 17 | @Override 18 | public LocalDate convertToEntityAttribute(Date sqlDate) { 19 | return (sqlDate == null ? null : sqlDate.toLocalDate()); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/model/Order.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.model; 2 | 3 | import java.io.Serializable; 4 | import java.math.BigDecimal; 5 | import java.time.LocalDate; 6 | 7 | import javax.persistence.Entity; 8 | import javax.persistence.Table; 9 | 10 | @Entity 11 | @Table(name = "ORDER_ITEM") 12 | public class Order extends BaseEntity implements Serializable { 13 | 14 | private static final long serialVersionUID = 1L; 15 | 16 | private String customerId; 17 | 18 | private String product; 19 | 20 | private String productCatalog; 21 | 22 | private LocalDate orderDate; 23 | 24 | private int quantity; 25 | 26 | private BigDecimal unitPrice; 27 | 28 | public Order() { 29 | } 30 | 31 | public String getCustomerId() { 32 | return customerId; 33 | } 34 | 35 | public void setCustomerId(String customerId) { 36 | this.customerId = customerId; 37 | } 38 | 39 | public String getProduct() { 40 | return product; 41 | } 42 | 43 | public void setProduct(String product) { 44 | this.product = product; 45 | } 46 | 47 | public String getProductCatalog() { 48 | return productCatalog; 49 | } 50 | 51 | public void setProductCatalog(String productCatalog) { 52 | this.productCatalog = productCatalog; 53 | } 54 | 55 | public LocalDate getOrderDate() { 56 | return orderDate; 57 | } 58 | 59 | public void setOrderDate(LocalDate orderDate) { 60 | this.orderDate = orderDate; 61 | } 62 | 63 | public int getQuantity() { 64 | return quantity; 65 | } 66 | 67 | public void setQuantity(int quantity) { 68 | this.quantity = quantity; 69 | } 70 | 71 | public BigDecimal getUnitPrice() { 72 | return unitPrice; 73 | } 74 | 75 | public void setUnitPrice(BigDecimal unitPrice) { 76 | this.unitPrice = unitPrice; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/model/Product.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.model; 2 | 3 | import java.io.Serializable; 4 | import java.math.BigDecimal; 5 | import java.time.LocalDate; 6 | 7 | import javax.persistence.Column; 8 | import javax.persistence.Entity; 9 | import javax.persistence.Table; 10 | 11 | @Entity 12 | @Table 13 | public class Product extends BaseEntity implements Serializable { 14 | 15 | private static final long serialVersionUID = 1L; 16 | 17 | @Column(nullable = true) 18 | private String name; 19 | 20 | @Column(length = 4000, nullable = true) 21 | private String description; 22 | 23 | private LocalDate effectiveDate; 24 | 25 | private LocalDate expirationDate; 26 | 27 | private BigDecimal unitPrice; 28 | 29 | private String productCatalogId; 30 | 31 | public Product() { 32 | } 33 | 34 | public String getName() { 35 | return name; 36 | } 37 | 38 | public void setName(String name) { 39 | this.name = name; 40 | } 41 | 42 | public String getDescription() { 43 | return description; 44 | } 45 | 46 | public void setDescription(String description) { 47 | this.description = description; 48 | } 49 | 50 | public LocalDate getEffectiveDate() { 51 | return effectiveDate; 52 | } 53 | 54 | public void setEffectiveDate(LocalDate effectiveDate) { 55 | this.effectiveDate = effectiveDate; 56 | } 57 | 58 | public LocalDate getExpirationDate() { 59 | return expirationDate; 60 | } 61 | 62 | public void setExpirationDate(LocalDate expirationDate) { 63 | this.expirationDate = expirationDate; 64 | } 65 | 66 | public BigDecimal getUnitPrice() { 67 | return unitPrice; 68 | } 69 | 70 | public void setUnitPrice(BigDecimal unitPrice) { 71 | this.unitPrice = unitPrice; 72 | } 73 | 74 | public String getProductCatalogId() { 75 | return productCatalogId; 76 | } 77 | 78 | public void setProductCatalogId(String productCatalogId) { 79 | this.productCatalogId = productCatalogId; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/model/ProductCatalog.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.model; 2 | 3 | import java.io.Serializable; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.Table; 8 | 9 | @Entity 10 | @Table 11 | public class ProductCatalog extends BaseEntity implements Serializable { 12 | 13 | private static final long serialVersionUID = 1L; 14 | 15 | @Column(nullable = true) 16 | private String name; 17 | 18 | @Column(length = 4000, nullable = true) 19 | private String description; 20 | 21 | public ProductCatalog() { 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public void setName(String name) { 29 | this.name = name; 30 | } 31 | 32 | public String getDescription() { 33 | return description; 34 | } 35 | 36 | public void setDescription(String description) { 37 | this.description = description; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.repository; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.repository.CrudRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import com.example.demo.shop.model.Order; 9 | 10 | @Repository 11 | public interface OrderRepository extends CrudRepository { 12 | 13 | List findByCustomerId(String customerId); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/repository/ProductCatalogRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | import com.example.demo.shop.model.ProductCatalog; 7 | 8 | @Repository 9 | public interface ProductCatalogRepository extends CrudRepository { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/repository/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.repository; 2 | 3 | import java.time.LocalDate; 4 | import java.util.List; 5 | 6 | import org.springframework.data.jpa.repository.Query; 7 | import org.springframework.data.repository.CrudRepository; 8 | import org.springframework.stereotype.Repository; 9 | 10 | import com.example.demo.shop.model.Product; 11 | 12 | @Repository 13 | public interface ProductRepository extends CrudRepository { 14 | 15 | List findByProductCatalogId(String productCatalogId); 16 | 17 | @Query("SELECT p FROM Product p WHERE p.productCatalogId = :productCatalogId and p.effectiveDate <= :date and (p.expirationDate is null or p.expirationDate > :date)") 18 | List findByProductCatalogIdOnDate(String productCatalogId, LocalDate date); 19 | 20 | @Query("SELECT p FROM Product p WHERE p.effectiveDate <= :date and (p.expirationDate is null or p.expirationDate > :date)") 21 | List findAllOnDate(LocalDate date); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/service/CustomerServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.service; 2 | 3 | import java.net.URI; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | import org.keycloak.KeycloakPrincipal; 8 | import org.keycloak.KeycloakSecurityContext; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.core.ParameterizedTypeReference; 11 | import org.springframework.http.HttpEntity; 12 | import org.springframework.http.HttpHeaders; 13 | import org.springframework.http.HttpMethod; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.client.RestTemplate; 17 | 18 | import com.example.demo.shop.exceptions.DoesNotExistException; 19 | import com.example.demo.shop.model.CustomerInfo; 20 | 21 | @Component 22 | public class CustomerServiceImpl { 23 | 24 | @Value("${keycloak.auth-server-url}") 25 | private String keycloakServerUrl; 26 | 27 | @Value("${keycloak.realm}") 28 | private String keycloakRealm; 29 | 30 | public CustomerServiceImpl() { 31 | } 32 | 33 | public List getCustomerByUsername(String username, 34 | KeycloakPrincipal principal) throws DoesNotExistException { 35 | KeycloakSecurityContext context = principal.getKeycloakSecurityContext(); 36 | HttpHeaders headers = new HttpHeaders(); 37 | headers.setContentType(MediaType.APPLICATION_JSON); 38 | headers.set("Authorization", "Bearer " + context.getTokenString()); 39 | 40 | StringBuilder sb = new StringBuilder(keycloakServerUrl); 41 | sb.append("/admin/realms/").append(keycloakRealm).append("/users"); 42 | sb.append("?username=").append(username); 43 | 44 | HttpEntity entity = new HttpEntity(headers); 45 | RestTemplate restTemplate = new RestTemplate(); 46 | CustomerInfo[] users = restTemplate 47 | .exchange(URI.create(sb.toString()), HttpMethod.GET, entity, CustomerInfo[].class).getBody(); 48 | if (users.length == 0) { 49 | throw new DoesNotExistException(username); 50 | } 51 | 52 | return Arrays.asList(Arrays.stream(users).toArray(CustomerInfo[]::new)); 53 | } 54 | 55 | public List getCustomers(KeycloakPrincipal principal) { 56 | KeycloakSecurityContext context = principal.getKeycloakSecurityContext(); 57 | HttpHeaders headers = new HttpHeaders(); 58 | headers.setContentType(MediaType.APPLICATION_JSON); 59 | headers.set("Authorization", "Bearer " + context.getTokenString()); 60 | 61 | StringBuilder sb = new StringBuilder(keycloakServerUrl); 62 | sb.append("/admin/realms/").append(keycloakRealm).append("/users"); 63 | 64 | HttpEntity entity = new HttpEntity(headers); 65 | RestTemplate restTemplate = new RestTemplate(); 66 | List users = restTemplate.exchange(URI.create(sb.toString()), HttpMethod.GET, entity, 67 | new ParameterizedTypeReference>() { 68 | }).getBody(); 69 | 70 | return users; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/service/OrderServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.service; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Optional; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Component; 9 | 10 | import com.example.demo.shop.exceptions.DoesNotExistException; 11 | import com.example.demo.shop.model.Order; 12 | import com.example.demo.shop.repository.OrderRepository; 13 | 14 | @Component 15 | public class OrderServiceImpl { 16 | 17 | @Autowired 18 | private OrderRepository orderRepository; 19 | 20 | public OrderServiceImpl() { 21 | } 22 | 23 | public Order getOrder(String id) throws DoesNotExistException { 24 | Optional order = orderRepository.findById(id); 25 | if (!order.isPresent()) { 26 | throw new DoesNotExistException("Order"); 27 | } 28 | return order.get(); 29 | } 30 | 31 | public List getOrdersByCustomer(String customerId) { 32 | List list = new ArrayList<>(); 33 | orderRepository.findByCustomerId(customerId).forEach(e -> list.add(e)); 34 | return list; 35 | } 36 | 37 | public List getOrders() { 38 | List list = new ArrayList<>(); 39 | orderRepository.findAll().forEach(e -> list.add(e)); 40 | return list; 41 | } 42 | 43 | public Order createOrder(String customerId, Order order) { 44 | order.setCustomerId(customerId); 45 | return orderRepository.save(order); 46 | } 47 | 48 | public Order updateOrder(String id, Order order) throws DoesNotExistException { 49 | this.getOrder(id); 50 | order.setId(id); 51 | return orderRepository.save(order); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/service/ProductCatalogServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.service; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Optional; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Component; 9 | 10 | import com.example.demo.shop.exceptions.DoesNotExistException; 11 | import com.example.demo.shop.model.ProductCatalog; 12 | import com.example.demo.shop.repository.ProductCatalogRepository; 13 | 14 | @Component 15 | public class ProductCatalogServiceImpl { 16 | 17 | @Autowired 18 | private ProductCatalogRepository productCatalogRepository; 19 | 20 | public ProductCatalogServiceImpl() { 21 | } 22 | 23 | public ProductCatalog getProductCatalog(String id) throws DoesNotExistException { 24 | Optional productCatalog = productCatalogRepository.findById(id); 25 | if (!productCatalog.isPresent()) { 26 | throw new DoesNotExistException("Product Catalog"); 27 | } 28 | return productCatalog.get(); 29 | } 30 | 31 | public List getProductCatalogs() { 32 | List list = new ArrayList<>(); 33 | productCatalogRepository.findAll().forEach(e -> list.add(e)); 34 | return list; 35 | } 36 | 37 | public ProductCatalog createProductCatalog(ProductCatalog productCatalog) { 38 | return productCatalogRepository.save(productCatalog); 39 | } 40 | 41 | public ProductCatalog updateProductCatalog(String id, ProductCatalog productCatalog) throws DoesNotExistException { 42 | this.getProductCatalog(id); 43 | productCatalog.setId(id); 44 | return productCatalogRepository.save(productCatalog); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /backend/src/main/java/com/example/demo/shop/service/ProductServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.shop.service; 2 | 3 | import java.time.LocalDate; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | 11 | import com.example.demo.shop.exceptions.DoesNotExistException; 12 | import com.example.demo.shop.model.Product; 13 | import com.example.demo.shop.repository.ProductRepository; 14 | 15 | @Component 16 | public class ProductServiceImpl { 17 | 18 | @Autowired 19 | private ProductRepository productRepository; 20 | 21 | public ProductServiceImpl() { 22 | } 23 | 24 | public Product getProduct(String id) throws DoesNotExistException { 25 | Optional product = productRepository.findById(id); 26 | if (!product.isPresent()) { 27 | throw new DoesNotExistException("Product"); 28 | } 29 | return product.get(); 30 | } 31 | 32 | public List getProductsByProductCatalog(String productCatalogId) { 33 | List list = new ArrayList<>(); 34 | productRepository.findByProductCatalogId(productCatalogId).forEach(e -> list.add(e)); 35 | return list; 36 | } 37 | 38 | public List getEffectiveProductsByProductCatalogOnDate(String productCatalogId, LocalDate date) { 39 | List list = new ArrayList<>(); 40 | productRepository.findByProductCatalogIdOnDate(productCatalogId, date).forEach(e -> list.add(e)); 41 | return list; 42 | } 43 | 44 | public List getProducts() { 45 | List list = new ArrayList<>(); 46 | productRepository.findAll().forEach(e -> list.add(e)); 47 | return list; 48 | } 49 | 50 | public List getEffectiveProductsOnDate(LocalDate date) { 51 | List list = new ArrayList<>(); 52 | productRepository.findAllOnDate(date).forEach(e -> list.add(e)); 53 | return list; 54 | } 55 | 56 | public Product createProduct(String productCatalogId, Product product) { 57 | product.setProductCatalogId(productCatalogId); 58 | return productRepository.save(product); 59 | } 60 | 61 | public Product updateProduct(String id, Product product) throws DoesNotExistException { 62 | this.getProduct(id); 63 | product.setId(id); 64 | return productRepository.save(product); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /backend/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Server Properties 2 | server.port=8081 3 | # Context path of the application. 4 | server.servlet.context-path=/shop 5 | 6 | # Keycloak Configuration 7 | keycloak.auth-server-url=http://localhost:8080/auth 8 | keycloak.realm=demo 9 | keycloak.resource=my-app 10 | keycloak.public-client=true 11 | keycloak.bearer-only = true 12 | 13 | keycloak.security-constraints[0].authRoles[0]=user 14 | keycloak.security-constraints[0].authRoles[1]=admin 15 | keycloak.security-constraints[0].securityCollections[0].methods[0]=GET 16 | keycloak.security-constraints[0].securityCollections[0].patterns[0]=/api/products/* 17 | keycloak.security-constraints[0].securityCollections[0].patterns[1]=/api/productcatalogs/* 18 | keycloak.security-constraints[0].securityCollections[1].methods[0]=GET 19 | keycloak.security-constraints[0].securityCollections[1].methods[1]=POST 20 | keycloak.security-constraints[0].securityCollections[1].methods[2]=PUT 21 | keycloak.security-constraints[0].securityCollections[1].patterns[0]=/api/orders/* 22 | 23 | keycloak.security-constraints[1].authRoles[0]=admin 24 | keycloak.security-constraints[1].securityCollections[0].patterns[0]=/api/customers/* 25 | keycloak.security-constraints[1].securityCollections[1].methods[0]=POST 26 | keycloak.security-constraints[1].securityCollections[1].methods[1]=PUT 27 | keycloak.security-constraints[1].securityCollections[1].patterns[0]=/api/products/* 28 | keycloak.security-constraints[1].securityCollections[1].patterns[1]=/api/productcatalogs/* 29 | 30 | # Keycloak Enable CORS 31 | keycloak.cors = true 32 | 33 | # Enabling H2 database web console at /h2-console 34 | spring.h2.console.enabled=true 35 | 36 | # Turn off automatic schema creation to avoid conflicts when using schema.sql to create tables 37 | spring.jpa.hibernate.ddl-auto=none 38 | 39 | # Logging JPA Queries to console 40 | logging.level.org.hibernate.SQL=DEBUG 41 | logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE 42 | spring.jpa.properties.hibernate.format_sql=true 43 | -------------------------------------------------------------------------------- /backend/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO PRODUCT_CATALOG (ID, NAME) VALUES ('a70a0447-9a7d-4b64-9890-b5616c1f309c', 'Bakery'); 2 | INSERT INTO PRODUCT_CATALOG (ID, NAME) VALUES ('75c36466-aeb2-445f-b3f0-be8b25f30db3', 'Dairy'); 3 | INSERT INTO PRODUCT_CATALOG (ID, NAME) VALUES ('94a0e5bb-373d-4288-8832-768b4b5319cb', 'Butchery'); 4 | INSERT INTO PRODUCT_CATALOG (ID, NAME) VALUES ('99edf5d9-7dea-4e94-87a4-43f869aa3c38', 'Fruit & Vegetables'); 5 | INSERT INTO PRODUCT_CATALOG (ID, NAME) VALUES ('b678d1e6-488c-45d5-a113-0077b60aa67b', 'Household'); 6 | 7 | INSERT INTO PRODUCT (ID, NAME, DESCRIPTION, EFFECTIVE_DATE, UNIT_PRICE, PRODUCT_CATALOG_ID) 8 | VALUES ('0013b1b6-329b-4b12-bfd7-066cea338343', 'Ciabatta Rolls', 'These sandwich rolls are at once incredibly flavorful and exceedingly light.', {ts '2018-12-21'}, 4.25, 'a70a0447-9a7d-4b64-9890-b5616c1f309c'); 9 | INSERT INTO PRODUCT (ID, NAME, DESCRIPTION, EFFECTIVE_DATE, UNIT_PRICE, PRODUCT_CATALOG_ID) 10 | VALUES ('0dac2a17-1af2-4a42-9449-2012d9559bc3', 'Pumpkin Pie', 'A secret blend of traditional spices and combine them with sweet and spicy pumpkin custard.', {ts '2018-12-21'}, 45.99, 'a70a0447-9a7d-4b64-9890-b5616c1f309c'); 11 | INSERT INTO PRODUCT (ID, NAME, DESCRIPTION, EFFECTIVE_DATE, UNIT_PRICE, PRODUCT_CATALOG_ID) 12 | VALUES ('20d25a1b-b804-4aea-8154-3d99b90c30f7', 'Milk', 'Refreshing and delicious, milk is ready for your crunchy cereal and morning coffee.', {ts '2018-12-21'}, 24.50, '75c36466-aeb2-445f-b3f0-be8b25f30db3'); 13 | INSERT INTO PRODUCT (ID, NAME, DESCRIPTION, EFFECTIVE_DATE, UNIT_PRICE, PRODUCT_CATALOG_ID) 14 | VALUES ('21ea1d67-bd16-4d80-acf3-3e7857f1c190', 'Ground Beef', 'Raised without antibiotics and full of flavor, this beef is the base of big, juicy burgers, savory meat loaf and rich Bolognese sauce.', {ts '2018-12-21'}, 100.50, '94a0e5bb-373d-4288-8832-768b4b5319cb'); 15 | INSERT INTO PRODUCT (ID, NAME, DESCRIPTION, EFFECTIVE_DATE, UNIT_PRICE, PRODUCT_CATALOG_ID) 16 | VALUES ('11928edc-31da-4686-8ff8-139c9af8f19a', 'Loin Chops', 'Flown in from the sheep-rich plains of Australia, these flavorful, juicy chops are ready to be barbecue.', {ts '2018-12-21'}, 76.50, '94a0e5bb-373d-4288-8832-768b4b5319cb'); 17 | INSERT INTO PRODUCT (ID, NAME, DESCRIPTION, EFFECTIVE_DATE, UNIT_PRICE, PRODUCT_CATALOG_ID) 18 | VALUES ('579718d9-5426-4dd4-a54b-b40c02b74097', 'Brocolli', 'A hearty and tasty vegetable which is rich in dozens of nutrients.', {ts '2018-12-21'}, 11.25, '99edf5d9-7dea-4e94-87a4-43f869aa3c38'); 19 | INSERT INTO PRODUCT (ID, NAME, DESCRIPTION, EFFECTIVE_DATE, UNIT_PRICE, PRODUCT_CATALOG_ID) 20 | VALUES ('622e1cf8-01f9-460b-9620-cebb1d406137', 'Water melon', 'All the sweetness, crunch, and knockout juiciness of the classic summertime melon.', {ts '2018-12-21'}, 89.90, '99edf5d9-7dea-4e94-87a4-43f869aa3c38'); 21 | INSERT INTO PRODUCT (ID, NAME, DESCRIPTION, EFFECTIVE_DATE, UNIT_PRICE, PRODUCT_CATALOG_ID) 22 | VALUES ('04dd8d6b-eb2a-43ad-86f5-02857e6632d8', 'Potato', 'Starchy with low moisture content, perfect for baked potatoes or french fries.', {ts '2018-12-21'}, 29.99, '99edf5d9-7dea-4e94-87a4-43f869aa3c38'); 23 | 24 | INSERT INTO ORDER_ITEM (ID, CUSTOMER_ID, PRODUCT, PRODUCT_CATALOG, ORDER_DATE, QUANTITY, UNIT_PRICE) 25 | VALUES ('22551188-6e15-44c1-9f07-3ad5a549ff77', 'spacehunter', 'Ciabatta Rolls', 'Bakery', {ts '2018-12-21'}, 6, 4.25); 26 | INSERT INTO ORDER_ITEM (ID, CUSTOMER_ID, PRODUCT, PRODUCT_CATALOG, ORDER_DATE, QUANTITY, UNIT_PRICE) 27 | VALUES ('6a487924-c277-460f-b107-0fd233590981', 'spacehunter', 'Ground Beef', 'Butchery', {ts '2018-12-21'}, 1, 100.50); 28 | INSERT INTO ORDER_ITEM (ID, CUSTOMER_ID, PRODUCT, PRODUCT_CATALOG, ORDER_DATE, QUANTITY, UNIT_PRICE) 29 | VALUES ('8e3c954e-6118-4bae-b708-e9c8b1e9c0c6', 'mythbuster', 'Loin Chops', 'Butchery', {ts '2018-12-21'}, 2, 76.50); 30 | INSERT INTO ORDER_ITEM (ID, CUSTOMER_ID, PRODUCT, PRODUCT_CATALOG, ORDER_DATE, QUANTITY, UNIT_PRICE) 31 | VALUES ('146d94bc-0556-4fdf-8bcc-6f61499f87bf', 'grilldad', 'Milk', 'Dairy', {ts '2018-12-21'}, 2, 24.50); 32 | INSERT INTO ORDER_ITEM (ID, CUSTOMER_ID, PRODUCT, PRODUCT_CATALOG, ORDER_DATE, QUANTITY, UNIT_PRICE) 33 | VALUES ('8444c41c-c12e-490e-a9e1-91fc76610ae3', 'grilldad', 'Ground Beef', 'Butchery', {ts '2018-12-21'}, 1, 100.50); 34 | INSERT INTO ORDER_ITEM (ID, CUSTOMER_ID, PRODUCT, PRODUCT_CATALOG, ORDER_DATE, QUANTITY, UNIT_PRICE) 35 | VALUES ('55c1aced-1b1b-4bcc-82fe-7812bf29c08a', 'grilldad', 'Potato', 'Fruit & Vegetables', {ts '2018-12-21'}, 1, 29.99); 36 | -------------------------------------------------------------------------------- /backend/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE PRODUCT_CATALOG ( 2 | ID VARCHAR(255) NOT NULL, 3 | NAME VARCHAR(255), 4 | DESCRIPTION VARCHAR(4000), 5 | PRIMARY KEY (ID) 6 | ); 7 | 8 | CREATE TABLE PRODUCT ( 9 | ID VARCHAR(255) NOT NULL, 10 | NAME VARCHAR(255), 11 | DESCRIPTION VARCHAR(4000), 12 | EFFECTIVE_DATE DATE NOT NULL, 13 | EXPIRATION_DATE DATE, 14 | UNIT_PRICE DECIMAL(19, 2), 15 | PRODUCT_CATALOG_ID VARCHAR(255) NOT NULL, 16 | PRIMARY KEY (ID) 17 | ); 18 | 19 | ALTER TABLE PRODUCT 20 | ADD CONSTRAINT PRODUCT_CATALOG_ID_FK_01 21 | FOREIGN KEY (PRODUCT_CATALOG_ID) 22 | REFERENCES PRODUCT_CATALOG(ID); 23 | 24 | CREATE TABLE ORDER_ITEM ( 25 | ID VARCHAR(255) NOT NULL, 26 | CUSTOMER_ID VARCHAR(255) NOT NULL, 27 | PRODUCT VARCHAR(255) NOT NULL, 28 | PRODUCT_CATALOG VARCHAR(255) NOT NULL, 29 | ORDER_DATE DATE NOT NULL, 30 | QUANTITY INT, 31 | UNIT_PRICE DECIMAL(19, 2), 32 | PRIMARY KEY (ID) 33 | ); 34 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # profiling files 12 | chrome-profiler-events.json 13 | speed-measure-plugin.json 14 | 15 | # IDEs and editors 16 | /.idea 17 | .project 18 | .classpath 19 | .c9/ 20 | *.launch 21 | .settings/ 22 | *.sublime-workspace 23 | 24 | # IDE - VSCode 25 | .vscode/* 26 | !.vscode/settings.json 27 | !.vscode/tasks.json 28 | !.vscode/launch.json 29 | !.vscode/extensions.json 30 | 31 | # misc 32 | /.sass-cache 33 | /connect.lock 34 | /coverage 35 | /libpeerconnection.log 36 | npm-debug.log 37 | yarn-error.log 38 | testem.log 39 | /typings 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Shopapp 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.1.3. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "shopapp": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "inlineStyle": true, 14 | "styleext": "scss", 15 | "spec": false 16 | }, 17 | "@schematics/angular:class": { 18 | "spec": false 19 | }, 20 | "@schematics/angular:directive": { 21 | "spec": false 22 | }, 23 | "@schematics/angular:guard": { 24 | "spec": false 25 | }, 26 | "@schematics/angular:module": { 27 | "spec": false 28 | }, 29 | "@schematics/angular:pipe": { 30 | "spec": false 31 | }, 32 | "@schematics/angular:service": { 33 | "spec": false 34 | } 35 | }, 36 | "architect": { 37 | "build": { 38 | "builder": "@angular-devkit/build-angular:browser", 39 | "options": { 40 | "outputPath": "dist/shopapp", 41 | "index": "src/index.html", 42 | "main": "src/main.ts", 43 | "polyfills": "src/polyfills.ts", 44 | "tsConfig": "src/tsconfig.app.json", 45 | "assets": [ 46 | "src/favicon.ico", 47 | "src/assets" 48 | ], 49 | "styles": [ 50 | "./node_modules/bootstrap/dist/css/bootstrap.min.css", 51 | "./node_modules/ngx-bootstrap/datepicker/bs-datepicker.css", 52 | "src/styles.scss" 53 | ], 54 | "scripts": [] 55 | }, 56 | "configurations": { 57 | "production": { 58 | "fileReplacements": [ 59 | { 60 | "replace": "src/environments/environment.ts", 61 | "with": "src/environments/environment.prod.ts" 62 | } 63 | ], 64 | "optimization": true, 65 | "outputHashing": "all", 66 | "sourceMap": false, 67 | "extractCss": true, 68 | "namedChunks": false, 69 | "aot": true, 70 | "extractLicenses": true, 71 | "vendorChunk": false, 72 | "buildOptimizer": true, 73 | "budgets": [ 74 | { 75 | "type": "initial", 76 | "maximumWarning": "2mb", 77 | "maximumError": "5mb" 78 | } 79 | ] 80 | } 81 | } 82 | }, 83 | "serve": { 84 | "builder": "@angular-devkit/build-angular:dev-server", 85 | "options": { 86 | "browserTarget": "shopapp:build" 87 | }, 88 | "configurations": { 89 | "production": { 90 | "browserTarget": "shopapp:build:production" 91 | } 92 | } 93 | }, 94 | "extract-i18n": { 95 | "builder": "@angular-devkit/build-angular:extract-i18n", 96 | "options": { 97 | "browserTarget": "shopapp:build" 98 | } 99 | }, 100 | "test": { 101 | "builder": "@angular-devkit/build-angular:karma", 102 | "options": { 103 | "main": "src/test.ts", 104 | "polyfills": "src/polyfills.ts", 105 | "tsConfig": "src/tsconfig.spec.json", 106 | "karmaConfig": "src/karma.conf.js", 107 | "styles": [ 108 | "./node_modules/bootstrap/dist/css/bootstrap.min.css", 109 | "./node_modules/ngx-bootstrap/datepicker/bs-datepicker.css", 110 | "src/styles.scss" 111 | ], 112 | "scripts": [], 113 | "assets": [ 114 | "src/favicon.ico", 115 | "src/assets" 116 | ] 117 | } 118 | }, 119 | "lint": { 120 | "builder": "@angular-devkit/build-angular:tslint", 121 | "options": { 122 | "tsConfig": [ 123 | "src/tsconfig.app.json", 124 | "src/tsconfig.spec.json" 125 | ], 126 | "exclude": [ 127 | "**/node_modules/**" 128 | ] 129 | } 130 | } 131 | } 132 | }, 133 | "shopapp-e2e": { 134 | "root": "e2e/", 135 | "projectType": "application", 136 | "prefix": "", 137 | "architect": { 138 | "e2e": { 139 | "builder": "@angular-devkit/build-angular:protractor", 140 | "options": { 141 | "protractorConfig": "e2e/protractor.conf.js", 142 | "devServerTarget": "shopapp:serve" 143 | }, 144 | "configurations": { 145 | "production": { 146 | "devServerTarget": "shopapp:serve:production" 147 | } 148 | } 149 | }, 150 | "lint": { 151 | "builder": "@angular-devkit/build-angular:tslint", 152 | "options": { 153 | "tsConfig": "e2e/tsconfig.e2e.json", 154 | "exclude": [ 155 | "**/node_modules/**" 156 | ] 157 | } 158 | } 159 | } 160 | } 161 | }, 162 | "defaultProject": "shopapp" 163 | } -------------------------------------------------------------------------------- /frontend/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /frontend/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getTitleText()).toEqual('Welcome to shopapp!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopapp", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --proxy-config proxy.conf.json", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~7.1.0", 15 | "@angular/common": "~7.1.0", 16 | "@angular/compiler": "~7.1.0", 17 | "@angular/core": "~7.1.0", 18 | "@angular/forms": "~7.1.0", 19 | "@angular/platform-browser": "~7.1.0", 20 | "@angular/platform-browser-dynamic": "~7.1.0", 21 | "@angular/router": "~7.1.0", 22 | "bootstrap": "^4.1.1", 23 | "core-js": "^2.5.4", 24 | "keycloak-angular": "^6.0.0", 25 | "ngx-bootstrap": "^3.1.3", 26 | "open-iconic": "^1.1.1", 27 | "rxjs": "~6.3.3", 28 | "tslib": "^1.9.0", 29 | "zone.js": "~0.8.26" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "~0.11.0", 33 | "@angular/cli": "~7.1.3", 34 | "@angular/compiler-cli": "~7.1.0", 35 | "@angular/language-service": "~7.1.0", 36 | "@types/jasmine": "~2.8.8", 37 | "@types/jasminewd2": "~2.0.3", 38 | "@types/node": "~8.9.4", 39 | "codelyzer": "~4.5.0", 40 | "jasmine-core": "~2.99.1", 41 | "jasmine-spec-reporter": "~4.2.1", 42 | "karma": "~3.1.1", 43 | "karma-chrome-launcher": "~2.2.0", 44 | "karma-coverage-istanbul-reporter": "~2.0.1", 45 | "karma-jasmine": "~1.1.2", 46 | "karma-jasmine-html-reporter": "^0.2.2", 47 | "protractor": "~5.4.0", 48 | "sass": "^1.69.4", 49 | "ts-node": "~7.0.0", 50 | "tslint": "~5.11.0", 51 | "typescript": "~3.1.6" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/shop/api/*": { 3 | "target": "http://localhost:8081" 4 | } 5 | } -------------------------------------------------------------------------------- /frontend/src/app/app-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, ActivatedRouteSnapshot } from '@angular/router'; 3 | import { KeycloakService, KeycloakAuthGuard } from 'keycloak-angular'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class AppAuthGuard extends KeycloakAuthGuard { 9 | 10 | constructor( 11 | protected router: Router, 12 | protected keycloakAngular: KeycloakService) { 13 | super(router, keycloakAngular); 14 | } 15 | 16 | isAccessAllowed(route: ActivatedRouteSnapshot): Promise { 17 | return new Promise(async (resolve, reject) => { 18 | if (!this.authenticated) { 19 | this.keycloakAngular.login(); 20 | return resolve(true); 21 | } 22 | 23 | const requiredRoles = route.data.roles; 24 | if (!requiredRoles || requiredRoles.length === 0) { 25 | return resolve(true); 26 | } else { 27 | if (!this.roles || this.roles.length === 0) { 28 | resolve(false); 29 | } 30 | let granted: boolean = false; 31 | for (const requiredRole of requiredRoles) { 32 | if (this.roles.indexOf(requiredRole) > -1) { 33 | granted = true; 34 | break; 35 | } 36 | } 37 | resolve(granted); 38 | } 39 | }); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ProductCatalogComponent } from './components/product-catalog/product-catalog.component'; 4 | import { CustomersComponent } from './components/customers/customers.component'; 5 | import { OrdersComponent } from './components/orders/orders.component'; 6 | import { CustomerOrdersComponent } from './components/customer-orders/customer-orders.component'; 7 | import { AppAuthGuard } from './app-auth.guard'; 8 | import { CartComponent } from './components/cart/cart.component'; 9 | 10 | const routes: Routes = [ 11 | { path: 'productcatalog', component: ProductCatalogComponent }, 12 | { path: 'customers', component: CustomersComponent, canActivate: [AppAuthGuard], data: { roles: ['admin'] } }, 13 | { path: 'orders', component: OrdersComponent, canActivate: [AppAuthGuard], data: { roles: ['admin'] } }, 14 | { path: 'customerorders/:username', component: CustomerOrdersComponent, canActivate: [AppAuthGuard], data: { roles: ['admin'] } }, 15 | { path: 'cart', component: CartComponent, canActivate: [AppAuthGuard] }, 16 | { path: '', component: ProductCatalogComponent }, 17 | { path: '**', redirectTo: '/' } 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [RouterModule.forRoot(routes)], 22 | exports: [RouterModule] 23 | }) 24 | export class AppRoutingModule { } 25 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 43 | 44 |
45 | 46 | 47 |
48 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { CustomerInfo } from './models/customer.info'; 5 | import { CustomerStore } from './stores/customer.store'; 6 | 7 | @Component({ 8 | selector: 'app-root', 9 | templateUrl: './app.component.html', 10 | styles: [` 11 | .count { 12 | padding: 2px 3px; 13 | z-index:15; 14 | position:relative; 15 | left: -10px; 16 | top:-10px 17 | } 18 | `] 19 | }) 20 | export class AppComponent implements OnInit { 21 | 22 | isCollapsed = true; 23 | customer$: Observable; 24 | 25 | constructor( 26 | private router: Router, 27 | private customerStore: CustomerStore) { 28 | this.customerStore.init(); 29 | } 30 | 31 | ngOnInit() { 32 | this.customer$ = this.customerStore.getAll$(); 33 | } 34 | 35 | doLogin(): void { 36 | this.customerStore.login(); 37 | } 38 | 39 | 40 | async doLogout() { 41 | await this.router.navigate(['/']); 42 | await this.customerStore.logout(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { NgModule, APP_INITIALIZER } from '@angular/core'; 4 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 5 | import { DatePipe } from '@angular/common'; 6 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 7 | 8 | import { CollapseModule, TooltipModule, ModalModule, BsDropdownModule } from 'ngx-bootstrap'; 9 | 10 | import { KeycloakService, KeycloakAngularModule } from 'keycloak-angular'; 11 | import { environment } from 'src/environments/environment'; 12 | 13 | import { AppRoutingModule } from './app-routing.module'; 14 | import { AppComponent } from './app.component'; 15 | import { TwoDigitDecimalNumberDirective } from './directives/two-digit-decimal-number.directive'; 16 | import { HttpErrorInterceptor} from './interceptor/http-error.interceptor'; 17 | 18 | import { ProductCatalogComponent } from './components/product-catalog/product-catalog.component'; 19 | import { ProductComponent } from './components/product/product.component'; 20 | import { CustomersComponent } from './components/customers/customers.component'; 21 | import { OrdersComponent } from './components/orders/orders.component'; 22 | import { CustomerOrdersComponent } from './components/customer-orders/customer-orders.component'; 23 | import { CartComponent } from './components/cart/cart.component'; 24 | import { CustomerStore } from './stores/customer.store'; 25 | import { AlertComponent } from './components/alert/alert.component'; 26 | 27 | export function kcInitializer(keycloak: KeycloakService): () => Promise { 28 | return (): Promise => { 29 | return new Promise(async (resolve, reject) => { 30 | try { 31 | await keycloak.init({ 32 | config: environment.keycloak, 33 | initOptions: { 34 | onLoad: 'login-required', 35 | checkLoginIframe: false 36 | }, 37 | enableBearerInterceptor: true 38 | }); 39 | resolve(); 40 | } catch (error) { 41 | console.log("Error thrown in init "+error); 42 | reject(error); 43 | } 44 | }); 45 | }; 46 | } 47 | 48 | @NgModule({ 49 | declarations: [ 50 | AppComponent, 51 | ProductCatalogComponent, 52 | ProductComponent, 53 | CustomersComponent, 54 | OrdersComponent, 55 | TwoDigitDecimalNumberDirective, 56 | CustomerOrdersComponent, 57 | CartComponent, 58 | AlertComponent 59 | ], 60 | imports: [ 61 | BrowserModule, 62 | HttpClientModule, 63 | AppRoutingModule, 64 | BrowserAnimationsModule, 65 | FormsModule, 66 | ReactiveFormsModule, 67 | KeycloakAngularModule, 68 | CollapseModule.forRoot(), 69 | TooltipModule.forRoot(), 70 | ModalModule.forRoot(), 71 | BsDropdownModule.forRoot() 72 | ], 73 | providers: [ 74 | DatePipe, 75 | CustomerStore, 76 | { provide: APP_INITIALIZER, useFactory: kcInitializer, multi: true, deps: [KeycloakService] }, 77 | { provide: HTTP_INTERCEPTORS, useClass: HttpErrorInterceptor, multi: true } 78 | ], 79 | bootstrap: [AppComponent] 80 | }) 81 | 82 | export class AppModule { } 83 | -------------------------------------------------------------------------------- /frontend/src/app/components/alert/alert.component.html: -------------------------------------------------------------------------------- 1 |
3 | {{message.text}} 4 |
-------------------------------------------------------------------------------- /frontend/src/app/components/alert/alert.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { AlertService } from 'src/app/services/alert.service'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'app-alert', 7 | templateUrl: './alert.component.html', 8 | styles: [] 9 | }) 10 | export class AlertComponent implements OnInit, OnDestroy { 11 | 12 | private subscription: Subscription; 13 | message: any; 14 | 15 | constructor(private alertService: AlertService) { } 16 | 17 | ngOnInit() { 18 | this.subscription = this.alertService.getMessage().subscribe(message => { 19 | this.message = message; 20 | }); 21 | } 22 | 23 | ngOnDestroy() { 24 | this.subscription.unsubscribe(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/app/components/cart/cart.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | Shopping Basket 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 |
ProductProduct CatalogDateQuantityUnit PriceTotalActions
23 |
No orders found
24 |
{{order.product}}{{order.productCatalog}}{{order.orderDate | date:'yyyy-MM-dd'}}{{order.quantity}}{{order.unitPrice | number:'1.2-2'}}{{order.quantity * order.unitPrice | number:'1.2-2'}} 34 | 35 |
39 | 40 | 41 | 44 | 82 | 83 |
-------------------------------------------------------------------------------- /frontend/src/app/components/cart/cart.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, TemplateRef } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 4 | import { DatePipe } from '@angular/common'; 5 | 6 | import { BsModalService } from 'ngx-bootstrap/modal'; 7 | import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service'; 8 | 9 | import { Order } from '../../models/order'; 10 | import { CustomerInfo } from '../../models/customer.info'; 11 | import { CustomerStore } from 'src/app/stores/customer.store'; 12 | import { AlertService } from 'src/app/services/alert.service'; 13 | 14 | @Component({ 15 | selector: 'app-cart', 16 | templateUrl: './cart.component.html', 17 | styles: [] 18 | }) 19 | export class CartComponent implements OnInit { 20 | 21 | selectedOrder: Order; 22 | orderForm: FormGroup; 23 | modalRef: BsModalRef; 24 | customer$: Observable; 25 | 26 | constructor( 27 | private formBuilder: FormBuilder, 28 | private modalService: BsModalService, 29 | private customerStore: CustomerStore, 30 | private alertService: AlertService) { 31 | this.customerStore.init(); 32 | } 33 | 34 | ngOnInit() { 35 | this.customer$ = this.customerStore.getAll$(); 36 | } 37 | 38 | openEditOrderModal(template: TemplateRef, order: Order) { 39 | let dp = new DatePipe(navigator.language); 40 | let p = 'y-MM-dd'; // YYYY-MM-DD 41 | let dtr = dp.transform(order.orderDate, p); 42 | this.orderForm = this.formBuilder.group({ 43 | 'id' : order.id, 44 | 'customerId': order.customerId, 45 | 'product' : [{value: order.product, disabled: true}], 46 | 'productCatalog' : [{value: order.productCatalog, disabled: true}], 47 | 'unitPrice' : [{value: order.unitPrice, disabled: true}], 48 | 'orderDate' : [{value: dtr, disabled: true}], 49 | 'quantity' : [order.quantity, Validators.required] 50 | }); 51 | this.selectedOrder = order; 52 | this.modalRef = this.modalService.show(template, {ignoreBackdropClick: true}); 53 | } 54 | 55 | // convenience getter for easy access to form fields 56 | get f() { 57 | return this.orderForm.controls; 58 | } 59 | 60 | onUpdateOrder() { 61 | // stop here if form is invalid 62 | if (this.orderForm.invalid) { 63 | return; 64 | } 65 | let order = this.orderForm.getRawValue(); 66 | this.customerStore.updateOrder(order); 67 | this.modalRef.hide(); 68 | this.alertService.success(`${order.product} order quantity updated from ${this.selectedOrder.quantity} to ${order.quantity}.`); 69 | }; 70 | 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/app/components/customer-orders/customer-orders.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | Orders for {{customer.firstName}} {{customer.lastName}} 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
ProductProduct CatalogDateQuantityUnit PriceTotal
22 |
No orders found
23 |
{{order.product}}{{order.productCatalog}}{{order.orderDate | date:'yyyy-MM-dd'}}{{order.quantity}}{{order.unitPrice | number:'1.2-2'}}{{order.quantity * order.unitPrice | number:'1.2-2'}}
35 |
36 | -------------------------------------------------------------------------------- /frontend/src/app/components/customer-orders/customer-orders.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { flatMap, tap } from 'rxjs/operators'; 4 | 5 | import { OrderService } from '../../services/order.service'; 6 | import { Order } from '../../models/order'; 7 | import { CustomerService } from '../../services/customer.service'; 8 | import { CustomerInfo } from '../../models/customer.info'; 9 | 10 | @Component({ 11 | selector: 'app-customer-orders', 12 | templateUrl: './customer-orders.component.html', 13 | styles: [] 14 | }) 15 | export class CustomerOrdersComponent implements OnInit { 16 | 17 | customer: CustomerInfo; 18 | 19 | constructor( 20 | private route: ActivatedRoute, 21 | private customerService: CustomerService, 22 | private orderService: OrderService) { } 23 | 24 | ngOnInit() { 25 | this.loadOrdersByUsername(this.route.snapshot.params['username']); 26 | } 27 | 28 | loadOrdersByUsername(username: string) { 29 | this.customerService.getCustomerByUsername(username).pipe( 30 | tap(data => this.customer = data), 31 | flatMap(customer => { 32 | return this.orderService.getOrdersByCustomer(customer.username).pipe( 33 | tap((orders: Order[]) => { 34 | customer.orders = orders; 35 | }) 36 | ) 37 | }) 38 | ).subscribe(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/app/components/customers/customers.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 |
FirstnameLastnameOrders
{{ customer.firstName }}{{ customer.lastName }} 15 | 0 Orders 16 | 17 | {{ customer.orders.length }} Orders 18 | 19 |
23 |
24 | -------------------------------------------------------------------------------- /frontend/src/app/components/customers/customers.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { from } from 'rxjs'; 3 | import { flatMap, tap } from 'rxjs/operators'; 4 | 5 | import { CustomerService } from '../../services/customer.service'; 6 | import { CustomerInfo } from '../../models/customer.info'; 7 | import { OrderService } from '../../services/order.service'; 8 | import { Order } from '../../models/order'; 9 | 10 | @Component({ 11 | selector: 'app-customers', 12 | templateUrl: './customers.component.html', 13 | styles: [] 14 | }) 15 | export class CustomersComponent implements OnInit { 16 | 17 | customers: CustomerInfo[] = []; 18 | 19 | constructor( 20 | private customerService: CustomerService, 21 | private orderService: OrderService) { } 22 | 23 | ngOnInit() { 24 | this.customerService.getCustomers().pipe( 25 | tap(data => this.customers = data), 26 | flatMap(customers => from(customers)), 27 | flatMap(customer => { 28 | return this.orderService.getOrdersByCustomer(customer.username).pipe( 29 | tap((orders: Order[]) => { 30 | customer.orders = orders; 31 | }) 32 | ) 33 | }) 34 | ).subscribe(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/app/components/orders/orders.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | Orders for {{customer.firstName}} {{customer.lastName}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
ProductProduct CatalogDateQuantityUnit PriceTotal
21 |
No orders found
22 |
{{order.product}}{{order.productCatalog}}{{order.orderDate | date:'yyyy-MM-dd'}}{{order.quantity}}{{order.unitPrice | number:'1.2-2'}}{{order.quantity * order.unitPrice | number:'1.2-2'}}
34 |
35 |
-------------------------------------------------------------------------------- /frontend/src/app/components/orders/orders.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { from } from 'rxjs'; 3 | import { flatMap, tap } from 'rxjs/operators'; 4 | 5 | import { OrderService } from '../../services/order.service'; 6 | import { Order } from '../../models/order'; 7 | import { CustomerService } from '../../services/customer.service'; 8 | import { CustomerInfo } from '../../models/customer.info'; 9 | 10 | @Component({ 11 | selector: 'app-orders', 12 | templateUrl: './orders.component.html', 13 | styles: [] 14 | }) 15 | export class OrdersComponent implements OnInit { 16 | 17 | customers: CustomerInfo[] = []; 18 | 19 | constructor( 20 | private orderService: OrderService, 21 | private customerService: CustomerService) { } 22 | 23 | ngOnInit() { 24 | this.customerService.getCustomers().pipe( 25 | tap(data => this.customers = data), 26 | flatMap(customers => from(customers)), 27 | flatMap(customer => { 28 | return this.orderService.getOrdersByCustomer(customer.username).pipe( 29 | tap((orders: Order[]) => { 30 | customer.orders = orders; 31 | }) 32 | ) 33 | }) 34 | ).subscribe(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/app/components/product-catalog/product-catalog.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Product Catalogs 6 |
Pick a category 7 |
8 |
    9 |
  • 10 |
    No product catalogs found
    11 |
  • 12 |
  • {{ productCatalog.name }} 14 | 15 |
  • 16 |
17 |
18 |
19 | 20 |
21 | 24 | 25 |
26 |
-------------------------------------------------------------------------------- /frontend/src/app/components/product-catalog/product-catalog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { ProductCatalogService } from '../../services/product-catalog.service'; 4 | import { ProductCatalog } from '../../models/product-catalog'; 5 | 6 | @Component({ 7 | selector: 'app-product-catalog', 8 | templateUrl: './product-catalog.component.html', 9 | styles: [] 10 | }) 11 | export class ProductCatalogComponent implements OnInit { 12 | 13 | selectedProductCatalog: ProductCatalog; 14 | productCatalogs: ProductCatalog[] = []; 15 | 16 | constructor(private productCatalogService: ProductCatalogService) { } 17 | 18 | ngOnInit() { 19 | this.productCatalogService.getProductCatalogs() 20 | .subscribe(data => { 21 | this.productCatalogs = data; 22 | }); 23 | } 24 | 25 | changeProductCatalog(productCatalog: ProductCatalog) { 26 | this.selectedProductCatalog = productCatalog; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/app/components/product/product.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
{{ productCatalogName }} Products
4 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 40 | 41 | 42 |
NameDescriptionUnit PriceActions
20 |
No products found
21 |
{{ product.name }}{{ product.description }}{{ product.unitPrice | number:'1.2-2' }} 28 |
29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 |
39 |
43 |
44 |
45 | 46 | 47 | 50 | 90 | 91 | 92 | 93 | 96 | 99 | 103 | 104 | 105 | 106 | 109 | 149 | 150 | 151 | 152 | 155 | 192 | -------------------------------------------------------------------------------- /frontend/src/app/components/product/product.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnChanges, SimpleChange, Input, TemplateRef, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { DatePipe } from '@angular/common'; 4 | 5 | import { BsModalService } from 'ngx-bootstrap/modal'; 6 | import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service'; 7 | 8 | import { ProductService } from '../../services/product.service'; 9 | import { Product } from '../../models/product'; 10 | import { Order } from '../../models/order'; 11 | import { Observable } from 'rxjs'; 12 | import { CustomerInfo } from 'src/app/models/customer.info'; 13 | import { CustomerStore } from 'src/app/stores/customer.store'; 14 | import { AlertService } from 'src/app/services/alert.service'; 15 | 16 | @Component({ 17 | selector: 'app-product', 18 | templateUrl: './product.component.html', 19 | styles: [] 20 | }) 21 | export class ProductComponent implements OnChanges, OnInit { 22 | 23 | @Input() productCatalogId: string; 24 | @Input() productCatalogName: string; 25 | 26 | modalRef: BsModalRef; 27 | 28 | products: Product[] = []; 29 | selectedProduct: Product; 30 | productForm: FormGroup; 31 | orderForm: FormGroup; 32 | customer$: Observable; 33 | 34 | constructor( 35 | private productService: ProductService, 36 | private formBuilder: FormBuilder, 37 | private modalService: BsModalService, 38 | private customerStore: CustomerStore, 39 | private alertService: AlertService) { 40 | this.customerStore.init(); 41 | } 42 | 43 | async ngOnInit() { 44 | this.customer$ = this.customerStore.getAll$(); 45 | } 46 | 47 | ngOnChanges(changes: {[propKey: string]: SimpleChange}) { 48 | if (changes['productCatalogId']) { 49 | this.getProductsByProductCatalog(this.productCatalogId); 50 | } 51 | } 52 | 53 | getProductsByProductCatalog(id: string) { 54 | this.productService.getEffectiveProductsByProductCatalogOnDate(id, new Date(Date.now())) 55 | .subscribe(data => { 56 | this.products = data; 57 | }); 58 | } 59 | 60 | openAddProductModal(template: TemplateRef) { 61 | let dp = new DatePipe(navigator.language); 62 | let p = 'y-MM-dd'; // YYYY-MM-DD 63 | let dtr = dp.transform(new Date(), p); 64 | this.productForm = this.formBuilder.group({ 65 | 'name' : [null, Validators.required], 66 | 'description' : [null, Validators.required], 67 | 'unitPrice' : [null, Validators.required], 68 | 'effectiveDate' : [dtr, Validators.required] 69 | }); 70 | this.modalRef = this.modalService.show(template, {ignoreBackdropClick: true}); 71 | } 72 | 73 | openDeleteProductModal(template: TemplateRef, product: Product) { 74 | this.selectedProduct = product; 75 | this.modalRef = this.modalService.show(template, {ignoreBackdropClick: true}); 76 | } 77 | 78 | openEditProductModal(template: TemplateRef, product: Product) { 79 | let dp = new DatePipe(navigator.language); 80 | let p = 'y-MM-dd'; // YYYY-MM-DD 81 | let dtr = dp.transform(product.effectiveDate, p); 82 | this.productForm = this.formBuilder.group({ 83 | 'id' : product.id, 84 | 'productCatalogId': product.productCatalogId, 85 | 'name' : [product.name, Validators.required], 86 | 'description' : [product.description, Validators.required], 87 | 'unitPrice' : [product.unitPrice, Validators.required], 88 | 'effectiveDate' : [dtr, Validators.required] 89 | }); 90 | this.modalRef = this.modalService.show(template, {ignoreBackdropClick: true}); 91 | } 92 | 93 | openCreateOrderModal(template: TemplateRef, product: Product) { 94 | let dp = new DatePipe(navigator.language); 95 | let p = 'y-MM-dd'; // YYYY-MM-DD 96 | let dtr = dp.transform(new Date(), p); 97 | this.orderForm = this.formBuilder.group({ 98 | 'product' : [{value: product.name, disabled: true}], 99 | 'productCatalog' : [{value: this.productCatalogName, disabled: true}], 100 | 'unitPrice' : [{value: product.unitPrice, disabled: true}], 101 | 'orderDate' : [{value: dtr, disabled: true}], 102 | 'quantity' : [null, Validators.required] 103 | }); 104 | this.modalRef = this.modalService.show(template, {ignoreBackdropClick: true}); 105 | } 106 | 107 | // convenience getter for easy access to form fields 108 | get f() { 109 | return this.productForm.controls; 110 | } 111 | 112 | onAddProduct() { 113 | // stop here if form is invalid 114 | if (this.productForm.invalid) { 115 | return; 116 | } 117 | let product = this.productForm.value; 118 | product.productCatalogId = this.productCatalogId; 119 | this.productService.createProduct(this.productCatalogId, product) 120 | .subscribe(res => { 121 | this.getProductsByProductCatalog(this.productCatalogId); 122 | this.modalRef.hide(); 123 | this.alertService.success(`Added ${product.name} product.`); 124 | }); 125 | }; 126 | 127 | onUpdateProduct() { 128 | // stop here if form is invalid 129 | if (this.productForm.invalid) { 130 | return; 131 | } 132 | let product = this.productForm.value; 133 | this.productService.updateProduct(product.id, product) 134 | .subscribe(res => { 135 | this.getProductsByProductCatalog(this.productCatalogId); 136 | this.modalRef.hide(); 137 | this.alertService.success(`Updated ${product.name} product.`); 138 | }); 139 | }; 140 | 141 | onDeleteProduct(product: Product) { 142 | product.expirationDate = new Date(); 143 | this.productService.updateProduct(product.id, product) 144 | .subscribe(res => { 145 | this.getProductsByProductCatalog(this.productCatalogId); 146 | this.selectedProduct = {} as Product; 147 | this.modalRef.hide(); 148 | this.alertService.success(`Deleted ${product.name} product.`); 149 | }); 150 | } 151 | 152 | onCreateOrder() { 153 | // stop here if form is invalid 154 | if (this.orderForm.invalid) { 155 | return; 156 | } 157 | let order = this.orderForm.getRawValue(); 158 | this.customerStore.addOrder(order); 159 | this.modalRef.hide(); 160 | this.alertService.success(`Order placed for ${order.quantity} ${order.product}.`); 161 | }; 162 | 163 | } 164 | -------------------------------------------------------------------------------- /frontend/src/app/directives/two-digit-decimal-number.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, HostListener } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appTwoDigitDecimalNumber]' 5 | }) 6 | export class TwoDigitDecimalNumberDirective { 7 | // Allow decimal numbers and negative values 8 | private regex: RegExp = new RegExp(/^\d*\.?\d{0,2}$/g); 9 | // Allow key codes for special events. Reflect : 10 | // Backspace, tab, end, home 11 | private specialKeys: Array = ['Backspace', 'Tab', 'End', 'Home', '-']; 12 | 13 | constructor(private el: ElementRef) { } 14 | 15 | @HostListener('keydown', ['$event']) 16 | onKeyDown(event: KeyboardEvent) { 17 | // Allow Backspace, tab, end, and home keys 18 | if (this.specialKeys.indexOf(event.key) !== -1) { 19 | return; 20 | } 21 | let current: string = this.el.nativeElement.value; 22 | let next: string = current.concat(event.key); 23 | if (next && !String(next).match(this.regex)) { 24 | event.preventDefault(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/app/interceptor/http-error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; 4 | import { Observable, throwError } from 'rxjs'; 5 | import { catchError } from 'rxjs/operators'; 6 | import { AlertService } from '../services/alert.service'; 7 | 8 | @Injectable() 9 | export class HttpErrorInterceptor implements HttpInterceptor { 10 | 11 | constructor(private alertService: AlertService) { } 12 | 13 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 14 | return next.handle(request).pipe( 15 | catchError((response: HttpErrorResponse) => { 16 | this.alertService.error(`${response.error.status}: ${response.error.message} ${response.error.error}`); 17 | return throwError(response); 18 | }) 19 | ); 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /frontend/src/app/models/customer.info.ts: -------------------------------------------------------------------------------- 1 | import { Order } from './order'; 2 | import { KeycloakProfile } from 'keycloak-js'; 3 | 4 | export interface CustomerInfo extends KeycloakProfile { 5 | orders?: Order[]; 6 | isLoggedIn?: boolean | false; 7 | isAdministrator?: boolean | false; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/app/models/order.ts: -------------------------------------------------------------------------------- 1 | export interface Order { 2 | id: string; 3 | customerId: string; 4 | product: string; 5 | productCatalog: string; 6 | orderDate: Date; 7 | quantity: number; 8 | unitPrice: number; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/app/models/product-catalog.ts: -------------------------------------------------------------------------------- 1 | export interface ProductCatalog { 2 | id: string; 3 | name: string; 4 | desc: string; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/app/models/product.ts: -------------------------------------------------------------------------------- 1 | export interface Product { 2 | id: string; 3 | name: string; 4 | description: string; 5 | effectiveDate: Date; 6 | expirationDate: Date; 7 | unitPrice: number; 8 | productCatalogId: string; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/app/services/alert.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject, Observable } from 'rxjs'; 3 | import { Router, NavigationStart } from '@angular/router'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class AlertService { 9 | 10 | private subject = new Subject(); 11 | private keepAfterNavigationChange = false; 12 | 13 | constructor(private router: Router) { 14 | // clear alert message on route change 15 | router.events.subscribe(event => { 16 | if (event instanceof NavigationStart) { 17 | if (this.keepAfterNavigationChange) { 18 | // only keep for a single location change 19 | this.keepAfterNavigationChange = false; 20 | } else { 21 | // clear alert 22 | this.subject.next(); 23 | } 24 | } 25 | }); 26 | } 27 | 28 | success(message: string, keepAfterNavigationChange = false) { 29 | this.keepAfterNavigationChange = keepAfterNavigationChange; 30 | this.subject.next({ type: 'success', text: message }); 31 | } 32 | 33 | error(message: string, keepAfterNavigationChange = false) { 34 | this.keepAfterNavigationChange = keepAfterNavigationChange; 35 | this.subject.next({ type: 'error', text: message }); 36 | } 37 | 38 | getMessage(): Observable { 39 | return this.subject.asObservable(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/app/services/customer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 4 | import { map} from 'rxjs/operators'; 5 | 6 | import { CustomerInfo } from '../models/customer.info'; 7 | 8 | const headers = new HttpHeaders().set('Content-Type', 'application/json'); 9 | const apiUrl = "/shop/api/customers"; 10 | 11 | @Injectable({ 12 | providedIn: 'root' 13 | }) 14 | export class CustomerService { 15 | 16 | constructor(private http: HttpClient) { } 17 | 18 | getCustomers (): Observable { 19 | return this.http.get(apiUrl, {headers}); 20 | } 21 | 22 | getCustomerByUsername(username: string): Observable { 23 | const params = new HttpParams().set('username', username); 24 | return this.http.get(apiUrl, {headers, params}) 25 | .pipe( 26 | map(customers => { 27 | return customers[0]; 28 | }) 29 | ); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/app/services/order.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 4 | 5 | import { Order } from '../models/order'; 6 | 7 | const headers = new HttpHeaders().set('Content-Type', 'application/json'); 8 | const apiUrl = "/shop/api/orders"; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class OrderService { 14 | 15 | constructor(private http: HttpClient) { } 16 | 17 | getOrdersByCustomer(customerId: string): Observable { 18 | const params = new HttpParams().set('customerId', customerId); 19 | return this.http.get(apiUrl, {headers, params}); 20 | } 21 | 22 | getOrders(): Observable { 23 | return this.http.get(apiUrl, {headers}); 24 | } 25 | 26 | createOrder (customerId: string, order: Order): Observable { 27 | const params = new HttpParams().set('customerId', customerId); 28 | return this.http.post(apiUrl, order, {headers, params}); 29 | } 30 | 31 | updateOrder (id: string, order: Order): Observable { 32 | return this.http.put(`${apiUrl}/${id}`, order, {headers}); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/app/services/product-catalog.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 4 | 5 | import { ProductCatalog } from '../models/product-catalog'; 6 | 7 | const headers = new HttpHeaders().set('Content-Type', 'application/json'); 8 | const apiUrl = "/shop/api/productcatalogs"; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class ProductCatalogService { 14 | 15 | constructor(private http: HttpClient) { } 16 | 17 | getProductCatalogs (): Observable { 18 | return this.http.get(apiUrl, {headers}); 19 | } 20 | 21 | getProductCatalog(id: string): Observable { 22 | return this.http.get(`${apiUrl}/${id}`, {headers}); 23 | } 24 | 25 | createProductCatalog (productCatalog: ProductCatalog): Observable { 26 | return this.http.post(apiUrl, productCatalog, {headers}); 27 | } 28 | 29 | updateProductCatalog (id: string, productCatalog: ProductCatalog): Observable { 30 | return this.http.put(`${apiUrl}/${id}`, productCatalog, {headers}); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/app/services/product.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 4 | import { DatePipe } from '@angular/common'; 5 | 6 | import { Product } from '../models/product'; 7 | 8 | const headers = new HttpHeaders().set('Content-Type', 'application/json'); 9 | const apiUrl = "/shop/api/products"; 10 | 11 | @Injectable({ 12 | providedIn: 'root' 13 | }) 14 | export class ProductService { 15 | 16 | constructor( 17 | private http: HttpClient, 18 | private datePipe: DatePipe) { } 19 | 20 | getProductsByProductCatalog(productCatalogId: string): Observable { 21 | const params = new HttpParams().set('productCatalogId', productCatalogId); 22 | return this.http.get(apiUrl, {headers, params}); 23 | } 24 | 25 | getEffectiveProductsByProductCatalogOnDate(productCatalogId: string, date: Date): Observable { 26 | const params = new HttpParams().set('productCatalogId', productCatalogId).append('date', this.datePipe.transform(date, 'yyyy-MM-dd')); 27 | return this.http.get(apiUrl, {headers, params}); 28 | } 29 | 30 | createProduct (productCatalogId: string, product: Product): Observable { 31 | const params = new HttpParams().set('productCatalogId', productCatalogId); 32 | return this.http.post(apiUrl, product, {headers, params}); 33 | } 34 | 35 | updateProduct (id: string, product: Product): Observable { 36 | return this.http.put(`${apiUrl}/${id}`, product, {headers}); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/app/stores/customer.store.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Injectable } from '@angular/core'; 3 | import { tap, map, flatMap } from 'rxjs/operators'; 4 | import { from } from 'rxjs'; 5 | 6 | import { KeycloakService } from 'keycloak-angular'; 7 | 8 | import { Store } from './store'; 9 | import { CustomerInfo } from '../models/customer.info'; 10 | import { OrderService } from '../services/order.service'; 11 | import { Order } from '../models/order'; 12 | 13 | @Injectable({ 14 | providedIn: 'root' 15 | }) 16 | export class CustomerStore extends Store { 17 | constructor( 18 | private keycloakService: KeycloakService, 19 | private orderService: OrderService) { 20 | super(); 21 | } 22 | 23 | init = (): void => { 24 | if (this.getAll()) { return; } 25 | 26 | if (this.keycloakService.isLoggedIn()) { 27 | from(this.keycloakService.loadUserProfile()).pipe( 28 | tap(this.store), 29 | map(() => { 30 | this.getAll().isLoggedIn = true; 31 | this.getAll().isAdministrator = this.keycloakService.isUserInRole("admin"); 32 | }), 33 | flatMap(() => { 34 | return this.orderService.getOrdersByCustomer(this.getAll().username).pipe( 35 | tap((orders: Order[]) => { 36 | this.getAll().orders = orders; 37 | }) 38 | ) 39 | }) 40 | ).subscribe(); 41 | } 42 | } 43 | 44 | logout = async (): Promise => { 45 | await this.keycloakService.logout(); 46 | this.store(undefined); 47 | } 48 | 49 | login(): void { 50 | this.keycloakService.login(); 51 | this.init(); 52 | } 53 | 54 | addOrder(order: Order) { 55 | this.orderService.createOrder(this.getAll().username, order) 56 | .subscribe(order => { 57 | this.getAll().orders.push(order); 58 | this.store(this.getAll()); 59 | }); 60 | } 61 | 62 | updateOrder(order: Order) { 63 | this.orderService.updateOrder(order.id, order).pipe( 64 | tap(resOrder => { 65 | let orders = this.getAll().orders; 66 | let orderIndex = this.getAll().orders.findIndex(item => item.id === order.id); 67 | 68 | orders[orderIndex] = { 69 | quantity: resOrder.quantity, 70 | ...order 71 | }; 72 | 73 | this.store(this.getAll()); 74 | }) 75 | ).subscribe(); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /frontend/src/app/stores/store.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BehaviorSubject, Observable } from 'rxjs'; 3 | 4 | export abstract class Store { 5 | 6 | private state$: BehaviorSubject = new BehaviorSubject(undefined); 7 | 8 | getAll = (): T => this.state$.getValue(); 9 | 10 | getAll$ = (): Observable => this.state$.asObservable(); 11 | 12 | store = (nextState: T) => (this.state$.next(nextState)); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jannie-louwrens/spring-boot-keycloak-angular/b5daeea7e09de9d9eec3970bcccc34230ca49b8a/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend/src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | import { KeycloakConfig } from 'keycloak-angular'; 5 | 6 | // Add here your keycloak setup infos 7 | let keycloakConfig: KeycloakConfig = { 8 | url: 'http://localhost:8080/auth', 9 | realm: 'demo', 10 | clientId: 'my-app' 11 | }; 12 | 13 | export const environment = { 14 | production: false, 15 | keycloak: keycloakConfig 16 | }; 17 | 18 | /* 19 | * For easier debugging in development mode, you can import the following file 20 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 21 | * 22 | * This import should be commented out in production mode because it will have a negative impact 23 | * on performance if an error is thrown. 24 | */ 25 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 26 | -------------------------------------------------------------------------------- /frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jannie-louwrens/spring-boot-keycloak-angular/b5daeea7e09de9d9eec3970bcccc34230ca49b8a/frontend/src/favicon.ico -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Shopapp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** 38 | * If the application will be indexed by Google Search, the following is required. 39 | * Googlebot uses a renderer based on Chrome 41. 40 | * https://developers.google.com/search/docs/guides/rendering 41 | **/ 42 | // import 'core-js/es6/array'; 43 | 44 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 45 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 46 | 47 | /** IE10 and IE11 requires the following for the Reflect API. */ 48 | // import 'core-js/es6/reflect'; 49 | 50 | /** 51 | * Web Animations `@angular/platform-browser/animations` 52 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 53 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 54 | **/ 55 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 56 | 57 | /** 58 | * By default, zone.js will patch all possible macroTask and DomEvents 59 | * user can disable parts of macroTask/DomEvents patch by setting following flags 60 | * because those flags need to be set before `zone.js` being loaded, and webpack 61 | * will put import in the top of bundle, so user need to create a separate file 62 | * in this directory (for example: zone-flags.ts), and put the following flags 63 | * into that file, and then add the following code before importing zone.js. 64 | * import './zone-flags.ts'; 65 | * 66 | * The flags allowed in zone-flags.ts are listed here. 67 | * 68 | * The following flags will work for all browsers. 69 | * 70 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 71 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 72 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 73 | * 74 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 75 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 76 | * 77 | * (window as any).__Zone_enable_cross_context_check = true; 78 | * 79 | */ 80 | 81 | /*************************************************************************************************** 82 | * Zone JS is required by default for Angular itself. 83 | */ 84 | import 'zone.js/dist/zone'; // Included with Angular CLI. 85 | 86 | 87 | /*************************************************************************************************** 88 | * APPLICATION IMPORTS 89 | */ 90 | -------------------------------------------------------------------------------- /frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | @import '~bootstrap/dist/css/bootstrap.min.css'; 4 | @import '~open-iconic/font/css/open-iconic-bootstrap.min.css'; 5 | -------------------------------------------------------------------------------- /frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "use-input-property-decorator": true, 122 | "use-output-property-decorator": true, 123 | "use-host-property-decorator": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-life-cycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "component-class-suffix": true, 129 | "directive-class-suffix": true 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /images/keycloak_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jannie-louwrens/spring-boot-keycloak-angular/b5daeea7e09de9d9eec3970bcccc34230ca49b8a/images/keycloak_login.png -------------------------------------------------------------------------------- /images/shop.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jannie-louwrens/spring-boot-keycloak-angular/b5daeea7e09de9d9eec3970bcccc34230ca49b8a/images/shop.ico -------------------------------------------------------------------------------- /images/shopapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jannie-louwrens/spring-boot-keycloak-angular/b5daeea7e09de9d9eec3970bcccc34230ca49b8a/images/shopapp.png -------------------------------------------------------------------------------- /keycloak/demo-realm.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "demo", 3 | "realm" : "demo", 4 | "notBefore" : 1548006554, 5 | "revokeRefreshToken" : false, 6 | "refreshTokenMaxReuse" : 0, 7 | "accessTokenLifespan" : 300, 8 | "accessTokenLifespanForImplicitFlow" : 900, 9 | "ssoSessionIdleTimeout" : 1800, 10 | "ssoSessionMaxLifespan" : 36000, 11 | "offlineSessionIdleTimeout" : 2592000, 12 | "offlineSessionMaxLifespanEnabled" : false, 13 | "offlineSessionMaxLifespan" : 5184000, 14 | "accessCodeLifespan" : 60, 15 | "accessCodeLifespanUserAction" : 300, 16 | "accessCodeLifespanLogin" : 1800, 17 | "actionTokenGeneratedByAdminLifespan" : 43200, 18 | "actionTokenGeneratedByUserLifespan" : 300, 19 | "enabled" : true, 20 | "sslRequired" : "external", 21 | "registrationAllowed" : false, 22 | "registrationEmailAsUsername" : false, 23 | "rememberMe" : false, 24 | "verifyEmail" : false, 25 | "loginWithEmailAllowed" : true, 26 | "duplicateEmailsAllowed" : false, 27 | "resetPasswordAllowed" : false, 28 | "editUsernameAllowed" : false, 29 | "bruteForceProtected" : false, 30 | "permanentLockout" : false, 31 | "maxFailureWaitSeconds" : 900, 32 | "minimumQuickLoginWaitSeconds" : 60, 33 | "waitIncrementSeconds" : 60, 34 | "quickLoginCheckMilliSeconds" : 1000, 35 | "maxDeltaTimeSeconds" : 43200, 36 | "failureFactor" : 30, 37 | "roles" : { 38 | "realm" : [ { 39 | "id" : "4625d5bd-18c1-482d-9987-d184be1ada07", 40 | "name" : "admin", 41 | "composite" : true, 42 | "composites" : { 43 | "client" : { 44 | "realm-management" : [ "view-users" ] 45 | } 46 | }, 47 | "clientRole" : false, 48 | "containerId" : "demo", 49 | "attributes" : { } 50 | }, { 51 | "id" : "b47c9a1b-0fc7-40fb-b662-5e9a4e3aa32f", 52 | "name" : "user", 53 | "composite" : false, 54 | "clientRole" : false, 55 | "containerId" : "demo", 56 | "attributes" : { } 57 | }, { 58 | "id" : "80f5a75b-428a-452a-943e-074c5236858d", 59 | "name" : "offline_access", 60 | "description" : "${role_offline-access}", 61 | "composite" : false, 62 | "clientRole" : false, 63 | "containerId" : "demo", 64 | "attributes" : { } 65 | }, { 66 | "id" : "494cf855-6a75-4ec9-add2-f780c827242a", 67 | "name" : "uma_authorization", 68 | "description" : "${role_uma_authorization}", 69 | "composite" : false, 70 | "clientRole" : false, 71 | "containerId" : "demo", 72 | "attributes" : { } 73 | } ], 74 | "client" : { 75 | "realm-management" : [ { 76 | "id" : "6786e93d-5e84-44e7-9b90-c7f38f6f5490", 77 | "name" : "view-identity-providers", 78 | "description" : "${role_view-identity-providers}", 79 | "composite" : false, 80 | "clientRole" : true, 81 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 82 | "attributes" : { } 83 | }, { 84 | "id" : "f8b685fd-29aa-428a-87b5-60a6af0d0d6c", 85 | "name" : "query-clients", 86 | "description" : "${role_query-clients}", 87 | "composite" : false, 88 | "clientRole" : true, 89 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 90 | "attributes" : { } 91 | }, { 92 | "id" : "531ae500-28d4-46c5-90cf-e37e42b80f7b", 93 | "name" : "view-realm", 94 | "description" : "${role_view-realm}", 95 | "composite" : false, 96 | "clientRole" : true, 97 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 98 | "attributes" : { } 99 | }, { 100 | "id" : "e0bbff17-1c86-4d91-9483-13e0accc5f3d", 101 | "name" : "manage-clients", 102 | "description" : "${role_manage-clients}", 103 | "composite" : false, 104 | "clientRole" : true, 105 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 106 | "attributes" : { } 107 | }, { 108 | "id" : "f55f1610-db97-47ef-b3db-53a4d4f1d211", 109 | "name" : "manage-identity-providers", 110 | "description" : "${role_manage-identity-providers}", 111 | "composite" : false, 112 | "clientRole" : true, 113 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 114 | "attributes" : { } 115 | }, { 116 | "id" : "981f7291-f916-4733-8f7b-7924e3b8170e", 117 | "name" : "query-users", 118 | "description" : "${role_query-users}", 119 | "composite" : false, 120 | "clientRole" : true, 121 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 122 | "attributes" : { } 123 | }, { 124 | "id" : "b85977b8-cca7-4d94-a87c-d332723b5f9c", 125 | "name" : "manage-users", 126 | "description" : "${role_manage-users}", 127 | "composite" : false, 128 | "clientRole" : true, 129 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 130 | "attributes" : { } 131 | }, { 132 | "id" : "a4876785-60fe-4825-8ff6-98609c26b225", 133 | "name" : "query-realms", 134 | "description" : "${role_query-realms}", 135 | "composite" : false, 136 | "clientRole" : true, 137 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 138 | "attributes" : { } 139 | }, { 140 | "id" : "802cf9b3-40c0-4f12-8122-911f1f350bcf", 141 | "name" : "manage-authorization", 142 | "description" : "${role_manage-authorization}", 143 | "composite" : false, 144 | "clientRole" : true, 145 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 146 | "attributes" : { } 147 | }, { 148 | "id" : "077a3ace-84a8-4294-a4aa-cf90ab71e792", 149 | "name" : "view-events", 150 | "description" : "${role_view-events}", 151 | "composite" : false, 152 | "clientRole" : true, 153 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 154 | "attributes" : { } 155 | }, { 156 | "id" : "49f3913a-f38b-42d0-b0cf-4848f27075f8", 157 | "name" : "impersonation", 158 | "description" : "${role_impersonation}", 159 | "composite" : false, 160 | "clientRole" : true, 161 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 162 | "attributes" : { } 163 | }, { 164 | "id" : "763a723e-cd3a-4dd5-b70f-fd0dea6d6c78", 165 | "name" : "realm-admin", 166 | "description" : "${role_realm-admin}", 167 | "composite" : true, 168 | "composites" : { 169 | "client" : { 170 | "realm-management" : [ "view-identity-providers", "query-clients", "view-realm", "manage-clients", "manage-identity-providers", "query-users", "manage-users", "query-realms", "manage-authorization", "view-events", "impersonation", "view-authorization", "manage-realm", "manage-events", "query-groups", "view-clients", "view-users", "create-client" ] 171 | } 172 | }, 173 | "clientRole" : true, 174 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 175 | "attributes" : { } 176 | }, { 177 | "id" : "3116400b-3b5a-45bb-8adf-639e42018432", 178 | "name" : "view-authorization", 179 | "description" : "${role_view-authorization}", 180 | "composite" : false, 181 | "clientRole" : true, 182 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 183 | "attributes" : { } 184 | }, { 185 | "id" : "1d755db6-e6a3-4282-af64-21f792e0f796", 186 | "name" : "manage-realm", 187 | "description" : "${role_manage-realm}", 188 | "composite" : false, 189 | "clientRole" : true, 190 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 191 | "attributes" : { } 192 | }, { 193 | "id" : "7a5715e6-61cf-41f1-83be-bcf22dec306a", 194 | "name" : "manage-events", 195 | "description" : "${role_manage-events}", 196 | "composite" : false, 197 | "clientRole" : true, 198 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 199 | "attributes" : { } 200 | }, { 201 | "id" : "ec640459-57ee-46ab-8f31-9c488b1952f4", 202 | "name" : "query-groups", 203 | "description" : "${role_query-groups}", 204 | "composite" : false, 205 | "clientRole" : true, 206 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 207 | "attributes" : { } 208 | }, { 209 | "id" : "ac647372-6e17-4367-9c5d-49fab58c35c6", 210 | "name" : "view-clients", 211 | "description" : "${role_view-clients}", 212 | "composite" : true, 213 | "composites" : { 214 | "client" : { 215 | "realm-management" : [ "query-clients" ] 216 | } 217 | }, 218 | "clientRole" : true, 219 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 220 | "attributes" : { } 221 | }, { 222 | "id" : "86b0007e-c6e8-46da-aeaf-8595de302ad8", 223 | "name" : "create-client", 224 | "description" : "${role_create-client}", 225 | "composite" : false, 226 | "clientRole" : true, 227 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 228 | "attributes" : { } 229 | }, { 230 | "id" : "6015b6ff-26b3-4242-9667-ab76d3fa3d17", 231 | "name" : "view-users", 232 | "description" : "${role_view-users}", 233 | "composite" : true, 234 | "composites" : { 235 | "client" : { 236 | "realm-management" : [ "query-users", "query-groups" ] 237 | } 238 | }, 239 | "clientRole" : true, 240 | "containerId" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 241 | "attributes" : { } 242 | } ], 243 | "security-admin-console" : [ ], 244 | "admin-cli" : [ ], 245 | "broker" : [ { 246 | "id" : "3df90892-b9ff-48c2-8b81-f62f438215b7", 247 | "name" : "read-token", 248 | "description" : "${role_read-token}", 249 | "composite" : false, 250 | "clientRole" : true, 251 | "containerId" : "2fdfe915-7647-42d4-b7bd-dcec8e39c84b", 252 | "attributes" : { } 253 | } ], 254 | "account" : [ { 255 | "id" : "d2406e49-8be7-4caf-87b4-ad27d24dc2f6", 256 | "name" : "manage-account-links", 257 | "description" : "${role_manage-account-links}", 258 | "composite" : false, 259 | "clientRole" : true, 260 | "containerId" : "6bdb3893-bc1e-4667-b892-4140d28715a5", 261 | "attributes" : { } 262 | }, { 263 | "id" : "1e3d8ffe-bb0d-464e-b95b-7cfe70b60a38", 264 | "name" : "manage-account", 265 | "description" : "${role_manage-account}", 266 | "composite" : true, 267 | "composites" : { 268 | "client" : { 269 | "account" : [ "manage-account-links" ] 270 | } 271 | }, 272 | "clientRole" : true, 273 | "containerId" : "6bdb3893-bc1e-4667-b892-4140d28715a5", 274 | "attributes" : { } 275 | }, { 276 | "id" : "a3ef245d-4bb9-4ab5-8e81-adc8b9835378", 277 | "name" : "view-profile", 278 | "description" : "${role_view-profile}", 279 | "composite" : false, 280 | "clientRole" : true, 281 | "containerId" : "6bdb3893-bc1e-4667-b892-4140d28715a5", 282 | "attributes" : { } 283 | } ], 284 | "my-app" : [ ] 285 | } 286 | }, 287 | "groups" : [ ], 288 | "defaultRoles" : [ "uma_authorization", "offline_access" ], 289 | "requiredCredentials" : [ "password" ], 290 | "otpPolicyType" : "totp", 291 | "otpPolicyAlgorithm" : "HmacSHA1", 292 | "otpPolicyInitialCounter" : 0, 293 | "otpPolicyDigits" : 6, 294 | "otpPolicyLookAheadWindow" : 1, 295 | "otpPolicyPeriod" : 30, 296 | "otpSupportedApplications" : [ "FreeOTP", "Google Authenticator" ], 297 | "users" : [ { 298 | "id" : "e193758a-acc6-43f5-b5a7-e88d251c8b69", 299 | "createdTimestamp" : 1547909685362, 300 | "username" : "grilldad", 301 | "enabled" : true, 302 | "totp" : false, 303 | "emailVerified" : false, 304 | "firstName" : "Jim", 305 | "lastName" : "Long", 306 | "email" : "jim.long@example.com", 307 | "credentials" : [ { 308 | "type" : "password", 309 | "hashedSaltedValue" : "RXPcvHaUVW91YGXe3XR4LVNeAGj5yx6vgkbrPvsFXSiuE6Gt01BdJMlE0WfZ9HrwH2y5o2YyWZjAj6McHP/2pg==", 310 | "salt" : "K/jwydiEpsV1zDHSdQzIMw==", 311 | "hashIterations" : 27500, 312 | "counter" : 0, 313 | "algorithm" : "pbkdf2-sha256", 314 | "digits" : 0, 315 | "period" : 0, 316 | "createdDate" : 1547909700180, 317 | "config" : { } 318 | } ], 319 | "disableableCredentialTypes" : [ "password" ], 320 | "requiredActions" : [ ], 321 | "realmRoles" : [ "user", "offline_access", "uma_authorization" ], 322 | "clientRoles" : { 323 | "account" : [ "manage-account", "view-profile" ] 324 | }, 325 | "notBefore" : 0, 326 | "groups" : [ ] 327 | }, { 328 | "id" : "c9e1b98f-d081-4092-9f1b-68221cbf8f40", 329 | "createdTimestamp" : 1547909748545, 330 | "username" : "metalgear", 331 | "enabled" : true, 332 | "totp" : false, 333 | "emailVerified" : false, 334 | "firstName" : "Bob", 335 | "lastName" : "Knight", 336 | "email" : "bob.knight@example.com", 337 | "credentials" : [ { 338 | "type" : "password", 339 | "hashedSaltedValue" : "G/cMFZTkBdqUkO0Pgmh2PWKxbC5I2cmPtZ9y5JDUGPnMJJpSI06pYmdF6xzUYgutyZmgdEaQKkHjBoSKeIxtlw==", 340 | "salt" : "VIAtDMXbhvz6lcakcq7X+g==", 341 | "hashIterations" : 27500, 342 | "counter" : 0, 343 | "algorithm" : "pbkdf2-sha256", 344 | "digits" : 0, 345 | "period" : 0, 346 | "createdDate" : 1547909761505, 347 | "config" : { } 348 | } ], 349 | "disableableCredentialTypes" : [ "password" ], 350 | "requiredActions" : [ ], 351 | "realmRoles" : [ "admin", "offline_access", "uma_authorization" ], 352 | "clientRoles" : { 353 | "account" : [ "manage-account", "view-profile" ] 354 | }, 355 | "notBefore" : 1548006393, 356 | "groups" : [ ] 357 | }, { 358 | "id" : "f34401ba-fdf2-4b69-be9c-bf00441613e2", 359 | "createdTimestamp" : 1547909789414, 360 | "username" : "mythbuster", 361 | "enabled" : true, 362 | "totp" : false, 363 | "emailVerified" : false, 364 | "firstName" : "Kate", 365 | "lastName" : "Wilson", 366 | "email" : "kate.wilson@example.com", 367 | "credentials" : [ { 368 | "type" : "password", 369 | "hashedSaltedValue" : "xnbyfbzqQhkh1VORTYKWON25lVVSYXeo6aMtc7NA14HyrNoLZ16Y/8valihfh8Rg3eVVttbzVTl+VrrxCuozWg==", 370 | "salt" : "Kt2ubyRFGBrvGlf2cZPY2w==", 371 | "hashIterations" : 27500, 372 | "counter" : 0, 373 | "algorithm" : "pbkdf2-sha256", 374 | "digits" : 0, 375 | "period" : 0, 376 | "createdDate" : 1547909801468, 377 | "config" : { } 378 | } ], 379 | "disableableCredentialTypes" : [ "password" ], 380 | "requiredActions" : [ ], 381 | "realmRoles" : [ "user", "offline_access", "uma_authorization" ], 382 | "clientRoles" : { 383 | "account" : [ "manage-account", "view-profile" ] 384 | }, 385 | "notBefore" : 0, 386 | "groups" : [ ] 387 | }, { 388 | "id" : "98f36c9d-a1fe-4b83-9a2a-613658934398", 389 | "createdTimestamp" : 1547909826975, 390 | "username" : "spacehunter", 391 | "enabled" : true, 392 | "totp" : false, 393 | "emailVerified" : false, 394 | "firstName" : "Victor", 395 | "lastName" : "Brown", 396 | "email" : "victor.brown@example.com", 397 | "credentials" : [ { 398 | "type" : "password", 399 | "hashedSaltedValue" : "It92tkcE7hcQYsjWOnoxjYZ/QaYtfU6Kux95VoP3kUjXT+KvNGiClM9Sfzt2AL5tADZr9sOZ4fOCVcoFcqHr4w==", 400 | "salt" : "7rqJV136TEogRoWK3XR9zA==", 401 | "hashIterations" : 27500, 402 | "counter" : 0, 403 | "algorithm" : "pbkdf2-sha256", 404 | "digits" : 0, 405 | "period" : 0, 406 | "createdDate" : 1547909836358, 407 | "config" : { } 408 | } ], 409 | "disableableCredentialTypes" : [ "password" ], 410 | "requiredActions" : [ ], 411 | "realmRoles" : [ "user", "offline_access", "uma_authorization" ], 412 | "clientRoles" : { 413 | "account" : [ "manage-account", "view-profile" ] 414 | }, 415 | "notBefore" : 0, 416 | "groups" : [ ] 417 | } ], 418 | "scopeMappings" : [ { 419 | "clientScope" : "offline_access", 420 | "roles" : [ "offline_access" ] 421 | } ], 422 | "clients" : [ { 423 | "id" : "6bdb3893-bc1e-4667-b892-4140d28715a5", 424 | "clientId" : "account", 425 | "name" : "${client_account}", 426 | "baseUrl" : "/auth/realms/demo/account", 427 | "surrogateAuthRequired" : false, 428 | "enabled" : true, 429 | "clientAuthenticatorType" : "client-secret", 430 | "secret" : "3b41663d-b862-4279-8725-65e0b7404351", 431 | "defaultRoles" : [ "manage-account", "view-profile" ], 432 | "redirectUris" : [ "/auth/realms/demo/account/*" ], 433 | "webOrigins" : [ ], 434 | "notBefore" : 0, 435 | "bearerOnly" : false, 436 | "consentRequired" : false, 437 | "standardFlowEnabled" : true, 438 | "implicitFlowEnabled" : false, 439 | "directAccessGrantsEnabled" : false, 440 | "serviceAccountsEnabled" : false, 441 | "publicClient" : false, 442 | "frontchannelLogout" : false, 443 | "protocol" : "openid-connect", 444 | "attributes" : { }, 445 | "authenticationFlowBindingOverrides" : { }, 446 | "fullScopeAllowed" : false, 447 | "nodeReRegistrationTimeout" : 0, 448 | "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], 449 | "optionalClientScopes" : [ "address", "phone", "offline_access" ] 450 | }, { 451 | "id" : "2a5367b9-ee8f-4b43-bf60-078caa7b5e03", 452 | "clientId" : "admin-cli", 453 | "name" : "${client_admin-cli}", 454 | "surrogateAuthRequired" : false, 455 | "enabled" : true, 456 | "clientAuthenticatorType" : "client-secret", 457 | "secret" : "728cdd7d-4d31-47db-91bf-9254ff4d017e", 458 | "redirectUris" : [ ], 459 | "webOrigins" : [ ], 460 | "notBefore" : 0, 461 | "bearerOnly" : false, 462 | "consentRequired" : false, 463 | "standardFlowEnabled" : false, 464 | "implicitFlowEnabled" : false, 465 | "directAccessGrantsEnabled" : true, 466 | "serviceAccountsEnabled" : false, 467 | "publicClient" : true, 468 | "frontchannelLogout" : false, 469 | "protocol" : "openid-connect", 470 | "attributes" : { }, 471 | "authenticationFlowBindingOverrides" : { }, 472 | "fullScopeAllowed" : false, 473 | "nodeReRegistrationTimeout" : 0, 474 | "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], 475 | "optionalClientScopes" : [ "address", "phone", "offline_access" ] 476 | }, { 477 | "id" : "2fdfe915-7647-42d4-b7bd-dcec8e39c84b", 478 | "clientId" : "broker", 479 | "name" : "${client_broker}", 480 | "surrogateAuthRequired" : false, 481 | "enabled" : true, 482 | "clientAuthenticatorType" : "client-secret", 483 | "secret" : "c501a99a-67dc-44e4-8036-81f62e3ea79c", 484 | "redirectUris" : [ ], 485 | "webOrigins" : [ ], 486 | "notBefore" : 0, 487 | "bearerOnly" : false, 488 | "consentRequired" : false, 489 | "standardFlowEnabled" : true, 490 | "implicitFlowEnabled" : false, 491 | "directAccessGrantsEnabled" : false, 492 | "serviceAccountsEnabled" : false, 493 | "publicClient" : false, 494 | "frontchannelLogout" : false, 495 | "protocol" : "openid-connect", 496 | "attributes" : { }, 497 | "authenticationFlowBindingOverrides" : { }, 498 | "fullScopeAllowed" : false, 499 | "nodeReRegistrationTimeout" : 0, 500 | "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], 501 | "optionalClientScopes" : [ "address", "phone", "offline_access" ] 502 | }, { 503 | "id" : "7165fd15-068a-446f-b0d6-7b268653100f", 504 | "clientId" : "my-app", 505 | "surrogateAuthRequired" : false, 506 | "enabled" : true, 507 | "clientAuthenticatorType" : "client-secret", 508 | "secret" : "5902eff4-0127-440d-92d5-f0c0682ff4d7", 509 | "redirectUris" : [ "http://localhoat:8081/*", "http://localhost:4200/*" ], 510 | "webOrigins" : [ "*" ], 511 | "notBefore" : 0, 512 | "bearerOnly" : false, 513 | "consentRequired" : false, 514 | "standardFlowEnabled" : true, 515 | "implicitFlowEnabled" : false, 516 | "directAccessGrantsEnabled" : true, 517 | "serviceAccountsEnabled" : false, 518 | "publicClient" : true, 519 | "frontchannelLogout" : false, 520 | "protocol" : "openid-connect", 521 | "attributes" : { 522 | "saml.assertion.signature" : "false", 523 | "saml.force.post.binding" : "false", 524 | "saml.multivalued.roles" : "false", 525 | "saml.encrypt" : "false", 526 | "saml.server.signature" : "false", 527 | "saml.server.signature.keyinfo.ext" : "false", 528 | "exclude.session.state.from.auth.response" : "false", 529 | "saml_force_name_id_format" : "false", 530 | "saml.client.signature" : "false", 531 | "tls.client.certificate.bound.access.tokens" : "false", 532 | "saml.authnstatement" : "false", 533 | "display.on.consent.screen" : "false", 534 | "saml.onetimeuse.condition" : "false" 535 | }, 536 | "authenticationFlowBindingOverrides" : { }, 537 | "fullScopeAllowed" : true, 538 | "nodeReRegistrationTimeout" : -1, 539 | "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], 540 | "optionalClientScopes" : [ "address", "phone", "offline_access" ] 541 | }, { 542 | "id" : "340df2da-7ec5-4aa5-817a-692bb8a753b4", 543 | "clientId" : "realm-management", 544 | "name" : "${client_realm-management}", 545 | "surrogateAuthRequired" : false, 546 | "enabled" : true, 547 | "clientAuthenticatorType" : "client-secret", 548 | "secret" : "d76e0fda-e7e1-444a-814e-876fe0e88814", 549 | "redirectUris" : [ ], 550 | "webOrigins" : [ ], 551 | "notBefore" : 0, 552 | "bearerOnly" : true, 553 | "consentRequired" : false, 554 | "standardFlowEnabled" : true, 555 | "implicitFlowEnabled" : false, 556 | "directAccessGrantsEnabled" : false, 557 | "serviceAccountsEnabled" : false, 558 | "publicClient" : false, 559 | "frontchannelLogout" : false, 560 | "protocol" : "openid-connect", 561 | "attributes" : { }, 562 | "authenticationFlowBindingOverrides" : { }, 563 | "fullScopeAllowed" : false, 564 | "nodeReRegistrationTimeout" : 0, 565 | "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], 566 | "optionalClientScopes" : [ "address", "phone", "offline_access" ] 567 | }, { 568 | "id" : "b3e6c205-5df1-4017-8306-b6afd541327e", 569 | "clientId" : "security-admin-console", 570 | "name" : "${client_security-admin-console}", 571 | "baseUrl" : "/auth/admin/demo/console/index.html", 572 | "surrogateAuthRequired" : false, 573 | "enabled" : true, 574 | "clientAuthenticatorType" : "client-secret", 575 | "secret" : "4557ae58-0f34-42fd-92ed-6a5d5a80a533", 576 | "redirectUris" : [ "/auth/admin/demo/console/*" ], 577 | "webOrigins" : [ ], 578 | "notBefore" : 0, 579 | "bearerOnly" : false, 580 | "consentRequired" : false, 581 | "standardFlowEnabled" : true, 582 | "implicitFlowEnabled" : false, 583 | "directAccessGrantsEnabled" : false, 584 | "serviceAccountsEnabled" : false, 585 | "publicClient" : true, 586 | "frontchannelLogout" : false, 587 | "protocol" : "openid-connect", 588 | "attributes" : { }, 589 | "authenticationFlowBindingOverrides" : { }, 590 | "fullScopeAllowed" : false, 591 | "nodeReRegistrationTimeout" : 0, 592 | "protocolMappers" : [ { 593 | "id" : "be32c435-a1c3-4fb8-a4d8-859061e35b12", 594 | "name" : "locale", 595 | "protocol" : "openid-connect", 596 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 597 | "consentRequired" : false, 598 | "config" : { 599 | "userinfo.token.claim" : "true", 600 | "user.attribute" : "locale", 601 | "id.token.claim" : "true", 602 | "access.token.claim" : "true", 603 | "claim.name" : "locale", 604 | "jsonType.label" : "String" 605 | } 606 | } ], 607 | "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], 608 | "optionalClientScopes" : [ "address", "phone", "offline_access" ] 609 | } ], 610 | "clientScopes" : [ { 611 | "id" : "196ad9c2-fac3-49b3-bcd8-5bef9b010615", 612 | "name" : "address", 613 | "description" : "OpenID Connect built-in scope: address", 614 | "protocol" : "openid-connect", 615 | "attributes" : { 616 | "include.in.token.scope" : "true", 617 | "display.on.consent.screen" : "true", 618 | "consent.screen.text" : "${addressScopeConsentText}" 619 | }, 620 | "protocolMappers" : [ { 621 | "id" : "05d20d0f-8c89-4ece-b591-d5c830d51bda", 622 | "name" : "address", 623 | "protocol" : "openid-connect", 624 | "protocolMapper" : "oidc-address-mapper", 625 | "consentRequired" : false, 626 | "config" : { 627 | "user.attribute.formatted" : "formatted", 628 | "user.attribute.country" : "country", 629 | "user.attribute.postal_code" : "postal_code", 630 | "userinfo.token.claim" : "true", 631 | "user.attribute.street" : "street", 632 | "id.token.claim" : "true", 633 | "user.attribute.region" : "region", 634 | "access.token.claim" : "true", 635 | "user.attribute.locality" : "locality" 636 | } 637 | } ] 638 | }, { 639 | "id" : "d773ba00-4a4e-4c59-acec-9525c71aa8de", 640 | "name" : "email", 641 | "description" : "OpenID Connect built-in scope: email", 642 | "protocol" : "openid-connect", 643 | "attributes" : { 644 | "include.in.token.scope" : "true", 645 | "display.on.consent.screen" : "true", 646 | "consent.screen.text" : "${emailScopeConsentText}" 647 | }, 648 | "protocolMappers" : [ { 649 | "id" : "d5e6a017-09de-48ee-b88f-1992ee7a9bb9", 650 | "name" : "email verified", 651 | "protocol" : "openid-connect", 652 | "protocolMapper" : "oidc-usermodel-property-mapper", 653 | "consentRequired" : false, 654 | "config" : { 655 | "userinfo.token.claim" : "true", 656 | "user.attribute" : "emailVerified", 657 | "id.token.claim" : "true", 658 | "access.token.claim" : "true", 659 | "claim.name" : "email_verified", 660 | "jsonType.label" : "boolean" 661 | } 662 | }, { 663 | "id" : "58ffcc67-5ff2-42c9-ac47-0709f4e17819", 664 | "name" : "email", 665 | "protocol" : "openid-connect", 666 | "protocolMapper" : "oidc-usermodel-property-mapper", 667 | "consentRequired" : false, 668 | "config" : { 669 | "userinfo.token.claim" : "true", 670 | "user.attribute" : "email", 671 | "id.token.claim" : "true", 672 | "access.token.claim" : "true", 673 | "claim.name" : "email", 674 | "jsonType.label" : "String" 675 | } 676 | } ] 677 | }, { 678 | "id" : "eb76169f-87a7-49a5-a0bc-3760e6bb4f2b", 679 | "name" : "offline_access", 680 | "description" : "OpenID Connect built-in scope: offline_access", 681 | "protocol" : "openid-connect", 682 | "attributes" : { 683 | "consent.screen.text" : "${offlineAccessScopeConsentText}", 684 | "display.on.consent.screen" : "true" 685 | } 686 | }, { 687 | "id" : "2ce7f2e8-552b-443c-aabf-310338ed38fc", 688 | "name" : "phone", 689 | "description" : "OpenID Connect built-in scope: phone", 690 | "protocol" : "openid-connect", 691 | "attributes" : { 692 | "include.in.token.scope" : "true", 693 | "display.on.consent.screen" : "true", 694 | "consent.screen.text" : "${phoneScopeConsentText}" 695 | }, 696 | "protocolMappers" : [ { 697 | "id" : "a0723ddf-aaaa-400b-b046-ecadf2691d1e", 698 | "name" : "phone number verified", 699 | "protocol" : "openid-connect", 700 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 701 | "consentRequired" : false, 702 | "config" : { 703 | "userinfo.token.claim" : "true", 704 | "user.attribute" : "phoneNumberVerified", 705 | "id.token.claim" : "true", 706 | "access.token.claim" : "true", 707 | "claim.name" : "phone_number_verified", 708 | "jsonType.label" : "boolean" 709 | } 710 | }, { 711 | "id" : "fb336501-1435-4810-b439-cd0680372680", 712 | "name" : "phone number", 713 | "protocol" : "openid-connect", 714 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 715 | "consentRequired" : false, 716 | "config" : { 717 | "userinfo.token.claim" : "true", 718 | "user.attribute" : "phoneNumber", 719 | "id.token.claim" : "true", 720 | "access.token.claim" : "true", 721 | "claim.name" : "phone_number", 722 | "jsonType.label" : "String" 723 | } 724 | } ] 725 | }, { 726 | "id" : "73a245c2-8677-4c39-985b-160288cdb5cd", 727 | "name" : "profile", 728 | "description" : "OpenID Connect built-in scope: profile", 729 | "protocol" : "openid-connect", 730 | "attributes" : { 731 | "include.in.token.scope" : "true", 732 | "display.on.consent.screen" : "true", 733 | "consent.screen.text" : "${profileScopeConsentText}" 734 | }, 735 | "protocolMappers" : [ { 736 | "id" : "cb768568-bb59-4497-96b7-fa13776be6e2", 737 | "name" : "updated at", 738 | "protocol" : "openid-connect", 739 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 740 | "consentRequired" : false, 741 | "config" : { 742 | "userinfo.token.claim" : "true", 743 | "user.attribute" : "updatedAt", 744 | "id.token.claim" : "true", 745 | "access.token.claim" : "true", 746 | "claim.name" : "updated_at", 747 | "jsonType.label" : "String" 748 | } 749 | }, { 750 | "id" : "9ae81102-3154-410b-a254-f214a36f055f", 751 | "name" : "middle name", 752 | "protocol" : "openid-connect", 753 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 754 | "consentRequired" : false, 755 | "config" : { 756 | "userinfo.token.claim" : "true", 757 | "user.attribute" : "middleName", 758 | "id.token.claim" : "true", 759 | "access.token.claim" : "true", 760 | "claim.name" : "middle_name", 761 | "jsonType.label" : "String" 762 | } 763 | }, { 764 | "id" : "a9ecea56-aff2-4d70-b80c-fbe1b7b2d554", 765 | "name" : "username", 766 | "protocol" : "openid-connect", 767 | "protocolMapper" : "oidc-usermodel-property-mapper", 768 | "consentRequired" : false, 769 | "config" : { 770 | "userinfo.token.claim" : "true", 771 | "user.attribute" : "username", 772 | "id.token.claim" : "true", 773 | "access.token.claim" : "true", 774 | "claim.name" : "preferred_username", 775 | "jsonType.label" : "String" 776 | } 777 | }, { 778 | "id" : "dea6b464-bdcf-4b3a-ad48-e2a91f043e4e", 779 | "name" : "birthdate", 780 | "protocol" : "openid-connect", 781 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 782 | "consentRequired" : false, 783 | "config" : { 784 | "userinfo.token.claim" : "true", 785 | "user.attribute" : "birthdate", 786 | "id.token.claim" : "true", 787 | "access.token.claim" : "true", 788 | "claim.name" : "birthdate", 789 | "jsonType.label" : "String" 790 | } 791 | }, { 792 | "id" : "6922be9d-19c3-47d9-b57e-275539c07b42", 793 | "name" : "full name", 794 | "protocol" : "openid-connect", 795 | "protocolMapper" : "oidc-full-name-mapper", 796 | "consentRequired" : false, 797 | "config" : { 798 | "id.token.claim" : "true", 799 | "access.token.claim" : "true", 800 | "userinfo.token.claim" : "true" 801 | } 802 | }, { 803 | "id" : "14269c3c-d540-4bd0-8920-068a135002bc", 804 | "name" : "given name", 805 | "protocol" : "openid-connect", 806 | "protocolMapper" : "oidc-usermodel-property-mapper", 807 | "consentRequired" : false, 808 | "config" : { 809 | "userinfo.token.claim" : "true", 810 | "user.attribute" : "firstName", 811 | "id.token.claim" : "true", 812 | "access.token.claim" : "true", 813 | "claim.name" : "given_name", 814 | "jsonType.label" : "String" 815 | } 816 | }, { 817 | "id" : "507d686d-6c99-4ce0-9c06-dc8ab7aeb85d", 818 | "name" : "website", 819 | "protocol" : "openid-connect", 820 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 821 | "consentRequired" : false, 822 | "config" : { 823 | "userinfo.token.claim" : "true", 824 | "user.attribute" : "website", 825 | "id.token.claim" : "true", 826 | "access.token.claim" : "true", 827 | "claim.name" : "website", 828 | "jsonType.label" : "String" 829 | } 830 | }, { 831 | "id" : "e1e95991-8f37-4809-ac64-b376bfcf12cc", 832 | "name" : "locale", 833 | "protocol" : "openid-connect", 834 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 835 | "consentRequired" : false, 836 | "config" : { 837 | "userinfo.token.claim" : "true", 838 | "user.attribute" : "locale", 839 | "id.token.claim" : "true", 840 | "access.token.claim" : "true", 841 | "claim.name" : "locale", 842 | "jsonType.label" : "String" 843 | } 844 | }, { 845 | "id" : "7cd90f04-dacb-4b88-a612-754abd83443a", 846 | "name" : "picture", 847 | "protocol" : "openid-connect", 848 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 849 | "consentRequired" : false, 850 | "config" : { 851 | "userinfo.token.claim" : "true", 852 | "user.attribute" : "picture", 853 | "id.token.claim" : "true", 854 | "access.token.claim" : "true", 855 | "claim.name" : "picture", 856 | "jsonType.label" : "String" 857 | } 858 | }, { 859 | "id" : "c7bb1987-3bce-4ae6-89ff-04a0bcd51635", 860 | "name" : "nickname", 861 | "protocol" : "openid-connect", 862 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 863 | "consentRequired" : false, 864 | "config" : { 865 | "userinfo.token.claim" : "true", 866 | "user.attribute" : "nickname", 867 | "id.token.claim" : "true", 868 | "access.token.claim" : "true", 869 | "claim.name" : "nickname", 870 | "jsonType.label" : "String" 871 | } 872 | }, { 873 | "id" : "5b8e1d72-62bc-48c7-be32-d4be8e9eb778", 874 | "name" : "family name", 875 | "protocol" : "openid-connect", 876 | "protocolMapper" : "oidc-usermodel-property-mapper", 877 | "consentRequired" : false, 878 | "config" : { 879 | "userinfo.token.claim" : "true", 880 | "user.attribute" : "lastName", 881 | "id.token.claim" : "true", 882 | "access.token.claim" : "true", 883 | "claim.name" : "family_name", 884 | "jsonType.label" : "String" 885 | } 886 | }, { 887 | "id" : "674b7e75-1d09-4abb-b61b-6787bc0d7c98", 888 | "name" : "zoneinfo", 889 | "protocol" : "openid-connect", 890 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 891 | "consentRequired" : false, 892 | "config" : { 893 | "userinfo.token.claim" : "true", 894 | "user.attribute" : "zoneinfo", 895 | "id.token.claim" : "true", 896 | "access.token.claim" : "true", 897 | "claim.name" : "zoneinfo", 898 | "jsonType.label" : "String" 899 | } 900 | }, { 901 | "id" : "e29cf504-ad11-471e-979b-6d07935a448c", 902 | "name" : "profile", 903 | "protocol" : "openid-connect", 904 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 905 | "consentRequired" : false, 906 | "config" : { 907 | "userinfo.token.claim" : "true", 908 | "user.attribute" : "profile", 909 | "id.token.claim" : "true", 910 | "access.token.claim" : "true", 911 | "claim.name" : "profile", 912 | "jsonType.label" : "String" 913 | } 914 | }, { 915 | "id" : "9adffec5-346b-4b20-939e-c29c8bc19205", 916 | "name" : "gender", 917 | "protocol" : "openid-connect", 918 | "protocolMapper" : "oidc-usermodel-attribute-mapper", 919 | "consentRequired" : false, 920 | "config" : { 921 | "userinfo.token.claim" : "true", 922 | "user.attribute" : "gender", 923 | "id.token.claim" : "true", 924 | "access.token.claim" : "true", 925 | "claim.name" : "gender", 926 | "jsonType.label" : "String" 927 | } 928 | } ] 929 | }, { 930 | "id" : "becbd61f-8cda-4a0d-83d2-e8af000274cf", 931 | "name" : "role_list", 932 | "description" : "SAML role list", 933 | "protocol" : "saml", 934 | "attributes" : { 935 | "consent.screen.text" : "${samlRoleListScopeConsentText}", 936 | "display.on.consent.screen" : "true" 937 | }, 938 | "protocolMappers" : [ { 939 | "id" : "cfda291e-1f39-40ce-8626-b5b3415c3153", 940 | "name" : "role list", 941 | "protocol" : "saml", 942 | "protocolMapper" : "saml-role-list-mapper", 943 | "consentRequired" : false, 944 | "config" : { 945 | "single" : "false", 946 | "attribute.nameformat" : "Basic", 947 | "attribute.name" : "Role" 948 | } 949 | } ] 950 | }, { 951 | "id" : "10960510-ba96-40a6-88cf-afb14bb61f90", 952 | "name" : "roles", 953 | "description" : "OpenID Connect scope for add user roles to the access token", 954 | "protocol" : "openid-connect", 955 | "attributes" : { 956 | "include.in.token.scope" : "false", 957 | "display.on.consent.screen" : "true", 958 | "consent.screen.text" : "${rolesScopeConsentText}" 959 | }, 960 | "protocolMappers" : [ { 961 | "id" : "b220c335-fa8a-4f92-86a7-82d13b2f924e", 962 | "name" : "audience resolve", 963 | "protocol" : "openid-connect", 964 | "protocolMapper" : "oidc-audience-resolve-mapper", 965 | "consentRequired" : false, 966 | "config" : { } 967 | }, { 968 | "id" : "1a8415fb-103b-4525-872c-4344c1840c98", 969 | "name" : "realm roles", 970 | "protocol" : "openid-connect", 971 | "protocolMapper" : "oidc-usermodel-realm-role-mapper", 972 | "consentRequired" : false, 973 | "config" : { 974 | "user.attribute" : "foo", 975 | "access.token.claim" : "true", 976 | "claim.name" : "realm_access.roles", 977 | "jsonType.label" : "String", 978 | "multivalued" : "true" 979 | } 980 | }, { 981 | "id" : "310860db-4a52-43dc-b967-7c005cefe667", 982 | "name" : "client roles", 983 | "protocol" : "openid-connect", 984 | "protocolMapper" : "oidc-usermodel-client-role-mapper", 985 | "consentRequired" : false, 986 | "config" : { 987 | "user.attribute" : "foo", 988 | "access.token.claim" : "true", 989 | "claim.name" : "resource_access.${client_id}.roles", 990 | "jsonType.label" : "String", 991 | "multivalued" : "true" 992 | } 993 | } ] 994 | }, { 995 | "id" : "05a3fc34-53e7-4e01-84ea-9a5d5bc5919a", 996 | "name" : "web-origins", 997 | "description" : "OpenID Connect scope for add allowed web origins to the access token", 998 | "protocol" : "openid-connect", 999 | "attributes" : { 1000 | "include.in.token.scope" : "false", 1001 | "display.on.consent.screen" : "false", 1002 | "consent.screen.text" : "" 1003 | }, 1004 | "protocolMappers" : [ { 1005 | "id" : "41614593-eb67-4f4b-a1e6-fae3f4cd3aa4", 1006 | "name" : "allowed web origins", 1007 | "protocol" : "openid-connect", 1008 | "protocolMapper" : "oidc-allowed-origins-mapper", 1009 | "consentRequired" : false, 1010 | "config" : { } 1011 | } ] 1012 | } ], 1013 | "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins" ], 1014 | "defaultOptionalClientScopes" : [ "offline_access", "address", "phone" ], 1015 | "browserSecurityHeaders" : { 1016 | "contentSecurityPolicyReportOnly" : "", 1017 | "xContentTypeOptions" : "nosniff", 1018 | "xRobotsTag" : "none", 1019 | "xFrameOptions" : "SAMEORIGIN", 1020 | "xXSSProtection" : "1; mode=block", 1021 | "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", 1022 | "strictTransportSecurity" : "max-age=31536000; includeSubDomains" 1023 | }, 1024 | "smtpServer" : { }, 1025 | "eventsEnabled" : false, 1026 | "eventsListeners" : [ "jboss-logging" ], 1027 | "enabledEventTypes" : [ ], 1028 | "adminEventsEnabled" : false, 1029 | "adminEventsDetailsEnabled" : false, 1030 | "components" : { 1031 | "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { 1032 | "id" : "4d424b28-ca77-42c7-bab1-f38202c14a32", 1033 | "name" : "Max Clients Limit", 1034 | "providerId" : "max-clients", 1035 | "subType" : "anonymous", 1036 | "subComponents" : { }, 1037 | "config" : { 1038 | "max-clients" : [ "200" ] 1039 | } 1040 | }, { 1041 | "id" : "0a503143-bff1-4e70-80d8-4259654c2fb7", 1042 | "name" : "Trusted Hosts", 1043 | "providerId" : "trusted-hosts", 1044 | "subType" : "anonymous", 1045 | "subComponents" : { }, 1046 | "config" : { 1047 | "host-sending-registration-request-must-match" : [ "true" ], 1048 | "client-uris-must-match" : [ "true" ] 1049 | } 1050 | }, { 1051 | "id" : "2e09429a-524d-49bb-b85a-bcfd0aaba3c7", 1052 | "name" : "Allowed Client Scopes", 1053 | "providerId" : "allowed-client-templates", 1054 | "subType" : "anonymous", 1055 | "subComponents" : { }, 1056 | "config" : { 1057 | "allow-default-scopes" : [ "true" ] 1058 | } 1059 | }, { 1060 | "id" : "5433e3b6-309b-4748-8a70-550f94d4379b", 1061 | "name" : "Allowed Protocol Mapper Types", 1062 | "providerId" : "allowed-protocol-mappers", 1063 | "subType" : "authenticated", 1064 | "subComponents" : { }, 1065 | "config" : { 1066 | "allowed-protocol-mapper-types" : [ "oidc-address-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-full-name-mapper" ] 1067 | } 1068 | }, { 1069 | "id" : "eb2e4a34-d8f3-4212-be16-46c4536ffddb", 1070 | "name" : "Allowed Protocol Mapper Types", 1071 | "providerId" : "allowed-protocol-mappers", 1072 | "subType" : "anonymous", 1073 | "subComponents" : { }, 1074 | "config" : { 1075 | "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper" ] 1076 | } 1077 | }, { 1078 | "id" : "2d308d01-b92c-4392-81cf-35a70e3718f8", 1079 | "name" : "Allowed Client Scopes", 1080 | "providerId" : "allowed-client-templates", 1081 | "subType" : "authenticated", 1082 | "subComponents" : { }, 1083 | "config" : { 1084 | "allow-default-scopes" : [ "true" ] 1085 | } 1086 | }, { 1087 | "id" : "5d52409a-a01d-43f2-90a5-2e5a8b904062", 1088 | "name" : "Full Scope Disabled", 1089 | "providerId" : "scope", 1090 | "subType" : "anonymous", 1091 | "subComponents" : { }, 1092 | "config" : { } 1093 | }, { 1094 | "id" : "832e4f52-7955-452e-9db6-69c7883999b6", 1095 | "name" : "Consent Required", 1096 | "providerId" : "consent-required", 1097 | "subType" : "anonymous", 1098 | "subComponents" : { }, 1099 | "config" : { } 1100 | } ], 1101 | "org.keycloak.keys.KeyProvider" : [ { 1102 | "id" : "e11c6ba4-3c1a-4fbb-91a2-61e435198815", 1103 | "name" : "aes-generated", 1104 | "providerId" : "aes-generated", 1105 | "subComponents" : { }, 1106 | "config" : { 1107 | "kid" : [ "de6c83cb-6d9f-4c4b-b5f3-015c3cfe723f" ], 1108 | "secret" : [ "_0qAUlDf5_wwQ6oPpeC41Q" ], 1109 | "priority" : [ "100" ] 1110 | } 1111 | }, { 1112 | "id" : "0c117806-8191-48fc-8357-0c94ad01147c", 1113 | "name" : "hmac-generated", 1114 | "providerId" : "hmac-generated", 1115 | "subComponents" : { }, 1116 | "config" : { 1117 | "kid" : [ "0f78eb0f-90ad-4d49-88b7-b76d412dd7ad" ], 1118 | "secret" : [ "QAi_So2FkhDusEXnSkigWv_iQStRaAb86bvIpJeR-DXsKEmrBLLZwGyw-A_Migu2W6cM8yMAI2TWQ1ljI3MTmA" ], 1119 | "priority" : [ "100" ], 1120 | "algorithm" : [ "HS256" ] 1121 | } 1122 | }, { 1123 | "id" : "689f0929-79f2-4ca0-b602-1ba47dcefffe", 1124 | "name" : "rsa-generated", 1125 | "providerId" : "rsa-generated", 1126 | "subComponents" : { }, 1127 | "config" : { 1128 | "privateKey" : [ "MIIEowIBAAKCAQEAm04eZlOOQUm6OP0zBOwJOMA3zx2ryVRlC7csWkY1hnRGb8Ons8mmVTnrfDOWl2B9FwX+p3nFjuGs78YdCVovoI9UBm3++tVI81QzLjJyo+NsDRT2WCyvXVLDTt/G4SF6MVpCjT1oSirJy6HDONDUyHGlOlvETLzY6RvSmjWhTS4uUMjLyOHsAvMdI/QJnDDnOyJoohksHdA193JQq6mUgSbu8yoKsxU8ugaLEFtLJ/x/KhduCpPMxo+CJQNUNx069gLobI0j/7XrOevDdGYYP721m0xYbDRqwVFLszqjx9b616H79lG6i2e++BKSoNPTUbnMLaC4BMSNyd2GJRJZwQIDAQABAoIBACc+JhBVLKzypEuiLzIfMnVUnMWJCc9ls+Kx9lMZSo0e9G1pUwbq/UyrxgUsQ/EcQH+A1EgdTP49qCUuOkgSsdYwYVr+kF9xZW2W6RfTR4SCAtuLYHQHVoiOUSus/+QDQY0W8Zoe2itjhHof9eR1YE8F6GpGpeN+FJFSpnSw+IE3tB9IVKfInWPTQuS4JDEk3yuFaVntn8EPjgrHPAHubw9I2SA0eqP8h2qyONebcsJ98CpjwUhqxYzAQxuL8Vdpw0REX5LFPq5TJx5Q8NkkNexO6tVarntjRQqwviS0LvFLTHDkUvIZ72mbPOhSMyXKP3ra2QeF60V2Uu+XhJXlFsECgYEA6O89kfQ+8tWJxHApocyByIfEHGOpgdy3/MI0/SUZVbo8bvm+ONaGqgs+5CnLr1XFnerBrZRmI5cl39Mqm7XpTWOcvFz4T2o2Oh6BGseNTwmj8ClFmg+WvUeLy86522PC8x/1AxpLPZ/1doDZf99a6EW9tSvQVlVrtBKZEL1BV4cCgYEAqq8EXPOYwJdDpYCq2u33lilHttvixvdlp9uerKy9fmEiw5s4c72HOUe8d4GUJd7RbZwHKeOQoSdoZke4Xezfa0CoZpIantr5TOkU2AJZazaFQUxYTrddHU5lpCr1uDqdF9qKCZRYFncH0yuMvshwLf91Olcp4GVd6jJIWf2OhncCgYADyQd+HLL9LT0s5Qm4KGOfh51HPNNfWd/fiqRjzLuJqhobT5GoyMutbbta59b4hXNNqqf1EylJwMYLyhmyKBWffVPpt7UNTVOORDqi3cVNGIICgBkydLhlYQlDiZt5ljhvzQAlScMZQEUz5MokCtQmXCPGEu0yyfaTGY5FOF4cdwKBgQCXaUSMlIeoKUeYFKj8J2ef7KSygSMOb77dBkUfVumOp+qn7RVKDxJa7Nkyhl9+rMfJ3US5kybk3smNGghiQdP8F9KLkwZMb2ICKS4VZRgftLDHOUuJIL5YVghydq2drVlYJMSZNOBjpVnqqVJkl/hzxY8fnt0GM8X5EHwAYJ7DhwKBgF4YqA78h2Ntt9vDJ38hEV5xqGo5KQQpeC9z08MHg93RWgT7HpgwgkCo/rw/Osz/YUnXN+s8sMCYdSRkZs0+FhgToMmX/VJbfN/vqtflKfLSBzkQyasheUHERhRl60wBzdu33QY7rkBmPvBVbD0Uw7daM1mVtYepuWLZfN2rNZEp" ], 1129 | "certificate" : [ "MIIClzCCAX8CBgFoZpm79zANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARkZW1vMB4XDTE5MDExOTE0NTExN1oXDTI5MDExOTE0NTI1N1owDzENMAsGA1UEAwwEZGVtbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJtOHmZTjkFJujj9MwTsCTjAN88dq8lUZQu3LFpGNYZ0Rm/Dp7PJplU563wzlpdgfRcF/qd5xY7hrO/GHQlaL6CPVAZt/vrVSPNUMy4ycqPjbA0U9lgsr11Sw07fxuEhejFaQo09aEoqycuhwzjQ1MhxpTpbxEy82Okb0po1oU0uLlDIy8jh7ALzHSP0CZww5zsiaKIZLB3QNfdyUKuplIEm7vMqCrMVPLoGixBbSyf8fyoXbgqTzMaPgiUDVDcdOvYC6GyNI/+16znrw3RmGD+9tZtMWGw0asFRS7M6o8fW+teh+/ZRuotnvvgSkqDT01G5zC2guATEjcndhiUSWcECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAE51l+YtI5iHgWIdLFI3vk95xP8WbKQOCqzJ5+99wIe5mFilN3dusx79QoMppqsHpBh5Vb+6Yu+rSXthuz3IiBV3QctUd4dpca9/3bTdRm6LTdcU8ZY0mojGedztB6M7Ii2nw+MmOYEYlp0ARuDorB726XaMaD99R8m+dkCYFNrJFR2CuX0BMvNmA/PvOszeh+IughjYOkOdg7ICMj13MliaEHv28VfjVPLhvotZl90JXgvskNhT5AJq5xVe7sRAAhvFnpWEY/x3zZyKxDsZJhyysPafgnoFY7lCA8BWy0gRw0thlgd7rnV7t4vFA2re7JxGj3j88r429b8Hkh+fk1A==" ], 1130 | "priority" : [ "100" ] 1131 | } 1132 | } ] 1133 | }, 1134 | "internationalizationEnabled" : false, 1135 | "supportedLocales" : [ ], 1136 | "authenticationFlows" : [ { 1137 | "id" : "d4e8366a-b4f6-4e37-8433-0962ab5eafcb", 1138 | "alias" : "Handle Existing Account", 1139 | "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", 1140 | "providerId" : "basic-flow", 1141 | "topLevel" : false, 1142 | "builtIn" : true, 1143 | "authenticationExecutions" : [ { 1144 | "authenticator" : "idp-confirm-link", 1145 | "requirement" : "REQUIRED", 1146 | "priority" : 10, 1147 | "userSetupAllowed" : false, 1148 | "autheticatorFlow" : false 1149 | }, { 1150 | "authenticator" : "idp-email-verification", 1151 | "requirement" : "ALTERNATIVE", 1152 | "priority" : 20, 1153 | "userSetupAllowed" : false, 1154 | "autheticatorFlow" : false 1155 | }, { 1156 | "requirement" : "ALTERNATIVE", 1157 | "priority" : 30, 1158 | "flowAlias" : "Verify Existing Account by Re-authentication", 1159 | "userSetupAllowed" : false, 1160 | "autheticatorFlow" : true 1161 | } ] 1162 | }, { 1163 | "id" : "782ff7e4-f0c7-451e-95a9-7d6e3f225322", 1164 | "alias" : "Verify Existing Account by Re-authentication", 1165 | "description" : "Reauthentication of existing account", 1166 | "providerId" : "basic-flow", 1167 | "topLevel" : false, 1168 | "builtIn" : true, 1169 | "authenticationExecutions" : [ { 1170 | "authenticator" : "idp-username-password-form", 1171 | "requirement" : "REQUIRED", 1172 | "priority" : 10, 1173 | "userSetupAllowed" : false, 1174 | "autheticatorFlow" : false 1175 | }, { 1176 | "authenticator" : "auth-otp-form", 1177 | "requirement" : "OPTIONAL", 1178 | "priority" : 20, 1179 | "userSetupAllowed" : false, 1180 | "autheticatorFlow" : false 1181 | } ] 1182 | }, { 1183 | "id" : "c82f5bfd-9c84-406c-b45b-8d9e77c5d60e", 1184 | "alias" : "browser", 1185 | "description" : "browser based authentication", 1186 | "providerId" : "basic-flow", 1187 | "topLevel" : true, 1188 | "builtIn" : true, 1189 | "authenticationExecutions" : [ { 1190 | "authenticator" : "auth-cookie", 1191 | "requirement" : "ALTERNATIVE", 1192 | "priority" : 10, 1193 | "userSetupAllowed" : false, 1194 | "autheticatorFlow" : false 1195 | }, { 1196 | "authenticator" : "auth-spnego", 1197 | "requirement" : "DISABLED", 1198 | "priority" : 20, 1199 | "userSetupAllowed" : false, 1200 | "autheticatorFlow" : false 1201 | }, { 1202 | "authenticator" : "identity-provider-redirector", 1203 | "requirement" : "ALTERNATIVE", 1204 | "priority" : 25, 1205 | "userSetupAllowed" : false, 1206 | "autheticatorFlow" : false 1207 | }, { 1208 | "requirement" : "ALTERNATIVE", 1209 | "priority" : 30, 1210 | "flowAlias" : "forms", 1211 | "userSetupAllowed" : false, 1212 | "autheticatorFlow" : true 1213 | } ] 1214 | }, { 1215 | "id" : "6cc14178-0c8f-45fa-8a3e-1e435af5231f", 1216 | "alias" : "clients", 1217 | "description" : "Base authentication for clients", 1218 | "providerId" : "client-flow", 1219 | "topLevel" : true, 1220 | "builtIn" : true, 1221 | "authenticationExecutions" : [ { 1222 | "authenticator" : "client-secret", 1223 | "requirement" : "ALTERNATIVE", 1224 | "priority" : 10, 1225 | "userSetupAllowed" : false, 1226 | "autheticatorFlow" : false 1227 | }, { 1228 | "authenticator" : "client-jwt", 1229 | "requirement" : "ALTERNATIVE", 1230 | "priority" : 20, 1231 | "userSetupAllowed" : false, 1232 | "autheticatorFlow" : false 1233 | }, { 1234 | "authenticator" : "client-secret-jwt", 1235 | "requirement" : "ALTERNATIVE", 1236 | "priority" : 30, 1237 | "userSetupAllowed" : false, 1238 | "autheticatorFlow" : false 1239 | }, { 1240 | "authenticator" : "client-x509", 1241 | "requirement" : "ALTERNATIVE", 1242 | "priority" : 40, 1243 | "userSetupAllowed" : false, 1244 | "autheticatorFlow" : false 1245 | } ] 1246 | }, { 1247 | "id" : "0fc12fb7-6f5a-4da0-bdb3-c5f06c210918", 1248 | "alias" : "direct grant", 1249 | "description" : "OpenID Connect Resource Owner Grant", 1250 | "providerId" : "basic-flow", 1251 | "topLevel" : true, 1252 | "builtIn" : true, 1253 | "authenticationExecutions" : [ { 1254 | "authenticator" : "direct-grant-validate-username", 1255 | "requirement" : "REQUIRED", 1256 | "priority" : 10, 1257 | "userSetupAllowed" : false, 1258 | "autheticatorFlow" : false 1259 | }, { 1260 | "authenticator" : "direct-grant-validate-password", 1261 | "requirement" : "REQUIRED", 1262 | "priority" : 20, 1263 | "userSetupAllowed" : false, 1264 | "autheticatorFlow" : false 1265 | }, { 1266 | "authenticator" : "direct-grant-validate-otp", 1267 | "requirement" : "OPTIONAL", 1268 | "priority" : 30, 1269 | "userSetupAllowed" : false, 1270 | "autheticatorFlow" : false 1271 | } ] 1272 | }, { 1273 | "id" : "12d9ad8b-e42d-4ad4-b36b-340d7403a88a", 1274 | "alias" : "docker auth", 1275 | "description" : "Used by Docker clients to authenticate against the IDP", 1276 | "providerId" : "basic-flow", 1277 | "topLevel" : true, 1278 | "builtIn" : true, 1279 | "authenticationExecutions" : [ { 1280 | "authenticator" : "docker-http-basic-authenticator", 1281 | "requirement" : "REQUIRED", 1282 | "priority" : 10, 1283 | "userSetupAllowed" : false, 1284 | "autheticatorFlow" : false 1285 | } ] 1286 | }, { 1287 | "id" : "f25eeb7b-2502-40dd-8e2b-69cf934acc88", 1288 | "alias" : "first broker login", 1289 | "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", 1290 | "providerId" : "basic-flow", 1291 | "topLevel" : true, 1292 | "builtIn" : true, 1293 | "authenticationExecutions" : [ { 1294 | "authenticatorConfig" : "review profile config", 1295 | "authenticator" : "idp-review-profile", 1296 | "requirement" : "REQUIRED", 1297 | "priority" : 10, 1298 | "userSetupAllowed" : false, 1299 | "autheticatorFlow" : false 1300 | }, { 1301 | "authenticatorConfig" : "create unique user config", 1302 | "authenticator" : "idp-create-user-if-unique", 1303 | "requirement" : "ALTERNATIVE", 1304 | "priority" : 20, 1305 | "userSetupAllowed" : false, 1306 | "autheticatorFlow" : false 1307 | }, { 1308 | "requirement" : "ALTERNATIVE", 1309 | "priority" : 30, 1310 | "flowAlias" : "Handle Existing Account", 1311 | "userSetupAllowed" : false, 1312 | "autheticatorFlow" : true 1313 | } ] 1314 | }, { 1315 | "id" : "56c6fe06-c7bb-4aef-8263-3d2521f28ce1", 1316 | "alias" : "forms", 1317 | "description" : "Username, password, otp and other auth forms.", 1318 | "providerId" : "basic-flow", 1319 | "topLevel" : false, 1320 | "builtIn" : true, 1321 | "authenticationExecutions" : [ { 1322 | "authenticator" : "auth-username-password-form", 1323 | "requirement" : "REQUIRED", 1324 | "priority" : 10, 1325 | "userSetupAllowed" : false, 1326 | "autheticatorFlow" : false 1327 | }, { 1328 | "authenticator" : "auth-otp-form", 1329 | "requirement" : "OPTIONAL", 1330 | "priority" : 20, 1331 | "userSetupAllowed" : false, 1332 | "autheticatorFlow" : false 1333 | } ] 1334 | }, { 1335 | "id" : "04117b85-af47-4942-a724-e74d80f68fac", 1336 | "alias" : "http challenge", 1337 | "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", 1338 | "providerId" : "basic-flow", 1339 | "topLevel" : true, 1340 | "builtIn" : true, 1341 | "authenticationExecutions" : [ { 1342 | "authenticator" : "no-cookie-redirect", 1343 | "requirement" : "REQUIRED", 1344 | "priority" : 10, 1345 | "userSetupAllowed" : false, 1346 | "autheticatorFlow" : false 1347 | }, { 1348 | "authenticator" : "basic-auth", 1349 | "requirement" : "REQUIRED", 1350 | "priority" : 20, 1351 | "userSetupAllowed" : false, 1352 | "autheticatorFlow" : false 1353 | }, { 1354 | "authenticator" : "basic-auth-otp", 1355 | "requirement" : "DISABLED", 1356 | "priority" : 30, 1357 | "userSetupAllowed" : false, 1358 | "autheticatorFlow" : false 1359 | }, { 1360 | "authenticator" : "auth-spnego", 1361 | "requirement" : "DISABLED", 1362 | "priority" : 40, 1363 | "userSetupAllowed" : false, 1364 | "autheticatorFlow" : false 1365 | } ] 1366 | }, { 1367 | "id" : "73b29732-a43b-4a78-b449-30105274b44b", 1368 | "alias" : "registration", 1369 | "description" : "registration flow", 1370 | "providerId" : "basic-flow", 1371 | "topLevel" : true, 1372 | "builtIn" : true, 1373 | "authenticationExecutions" : [ { 1374 | "authenticator" : "registration-page-form", 1375 | "requirement" : "REQUIRED", 1376 | "priority" : 10, 1377 | "flowAlias" : "registration form", 1378 | "userSetupAllowed" : false, 1379 | "autheticatorFlow" : true 1380 | } ] 1381 | }, { 1382 | "id" : "e124968c-00b3-4ccc-8a4e-2b5b0629916a", 1383 | "alias" : "registration form", 1384 | "description" : "registration form", 1385 | "providerId" : "form-flow", 1386 | "topLevel" : false, 1387 | "builtIn" : true, 1388 | "authenticationExecutions" : [ { 1389 | "authenticator" : "registration-user-creation", 1390 | "requirement" : "REQUIRED", 1391 | "priority" : 20, 1392 | "userSetupAllowed" : false, 1393 | "autheticatorFlow" : false 1394 | }, { 1395 | "authenticator" : "registration-profile-action", 1396 | "requirement" : "REQUIRED", 1397 | "priority" : 40, 1398 | "userSetupAllowed" : false, 1399 | "autheticatorFlow" : false 1400 | }, { 1401 | "authenticator" : "registration-password-action", 1402 | "requirement" : "REQUIRED", 1403 | "priority" : 50, 1404 | "userSetupAllowed" : false, 1405 | "autheticatorFlow" : false 1406 | }, { 1407 | "authenticator" : "registration-recaptcha-action", 1408 | "requirement" : "DISABLED", 1409 | "priority" : 60, 1410 | "userSetupAllowed" : false, 1411 | "autheticatorFlow" : false 1412 | } ] 1413 | }, { 1414 | "id" : "9cc55198-6723-42b0-a25e-7e8cdf9d3cdb", 1415 | "alias" : "reset credentials", 1416 | "description" : "Reset credentials for a user if they forgot their password or something", 1417 | "providerId" : "basic-flow", 1418 | "topLevel" : true, 1419 | "builtIn" : true, 1420 | "authenticationExecutions" : [ { 1421 | "authenticator" : "reset-credentials-choose-user", 1422 | "requirement" : "REQUIRED", 1423 | "priority" : 10, 1424 | "userSetupAllowed" : false, 1425 | "autheticatorFlow" : false 1426 | }, { 1427 | "authenticator" : "reset-credential-email", 1428 | "requirement" : "REQUIRED", 1429 | "priority" : 20, 1430 | "userSetupAllowed" : false, 1431 | "autheticatorFlow" : false 1432 | }, { 1433 | "authenticator" : "reset-password", 1434 | "requirement" : "REQUIRED", 1435 | "priority" : 30, 1436 | "userSetupAllowed" : false, 1437 | "autheticatorFlow" : false 1438 | }, { 1439 | "authenticator" : "reset-otp", 1440 | "requirement" : "OPTIONAL", 1441 | "priority" : 40, 1442 | "userSetupAllowed" : false, 1443 | "autheticatorFlow" : false 1444 | } ] 1445 | }, { 1446 | "id" : "25ef0902-544f-4152-b130-38ec02564830", 1447 | "alias" : "saml ecp", 1448 | "description" : "SAML ECP Profile Authentication Flow", 1449 | "providerId" : "basic-flow", 1450 | "topLevel" : true, 1451 | "builtIn" : true, 1452 | "authenticationExecutions" : [ { 1453 | "authenticator" : "http-basic-authenticator", 1454 | "requirement" : "REQUIRED", 1455 | "priority" : 10, 1456 | "userSetupAllowed" : false, 1457 | "autheticatorFlow" : false 1458 | } ] 1459 | } ], 1460 | "authenticatorConfig" : [ { 1461 | "id" : "a3ef40d4-e9ef-4cec-9519-a4ccc35996df", 1462 | "alias" : "create unique user config", 1463 | "config" : { 1464 | "require.password.update.after.registration" : "false" 1465 | } 1466 | }, { 1467 | "id" : "9bf5019e-39bb-4cea-a7df-c21d1882b7e0", 1468 | "alias" : "review profile config", 1469 | "config" : { 1470 | "update.profile.on.first.login" : "missing" 1471 | } 1472 | } ], 1473 | "requiredActions" : [ { 1474 | "alias" : "CONFIGURE_TOTP", 1475 | "name" : "Configure OTP", 1476 | "providerId" : "CONFIGURE_TOTP", 1477 | "enabled" : true, 1478 | "defaultAction" : false, 1479 | "priority" : 10, 1480 | "config" : { } 1481 | }, { 1482 | "alias" : "terms_and_conditions", 1483 | "name" : "Terms and Conditions", 1484 | "providerId" : "terms_and_conditions", 1485 | "enabled" : false, 1486 | "defaultAction" : false, 1487 | "priority" : 20, 1488 | "config" : { } 1489 | }, { 1490 | "alias" : "UPDATE_PASSWORD", 1491 | "name" : "Update Password", 1492 | "providerId" : "UPDATE_PASSWORD", 1493 | "enabled" : true, 1494 | "defaultAction" : false, 1495 | "priority" : 30, 1496 | "config" : { } 1497 | }, { 1498 | "alias" : "UPDATE_PROFILE", 1499 | "name" : "Update Profile", 1500 | "providerId" : "UPDATE_PROFILE", 1501 | "enabled" : true, 1502 | "defaultAction" : false, 1503 | "priority" : 40, 1504 | "config" : { } 1505 | }, { 1506 | "alias" : "VERIFY_EMAIL", 1507 | "name" : "Verify Email", 1508 | "providerId" : "VERIFY_EMAIL", 1509 | "enabled" : true, 1510 | "defaultAction" : false, 1511 | "priority" : 50, 1512 | "config" : { } 1513 | } ], 1514 | "browserFlow" : "browser", 1515 | "registrationFlow" : "registration", 1516 | "directGrantFlow" : "direct grant", 1517 | "resetCredentialsFlow" : "reset credentials", 1518 | "clientAuthenticationFlow" : "clients", 1519 | "dockerAuthenticationFlow" : "docker auth", 1520 | "attributes" : { 1521 | "_browser_header.xXSSProtection" : "1; mode=block", 1522 | "_browser_header.xFrameOptions" : "SAMEORIGIN", 1523 | "_browser_header.strictTransportSecurity" : "max-age=31536000; includeSubDomains", 1524 | "permanentLockout" : "false", 1525 | "quickLoginCheckMilliSeconds" : "1000", 1526 | "_browser_header.xRobotsTag" : "none", 1527 | "maxFailureWaitSeconds" : "900", 1528 | "minimumQuickLoginWaitSeconds" : "60", 1529 | "failureFactor" : "30", 1530 | "actionTokenGeneratedByUserLifespan" : "300", 1531 | "maxDeltaTimeSeconds" : "43200", 1532 | "_browser_header.xContentTypeOptions" : "nosniff", 1533 | "offlineSessionMaxLifespan" : "5184000", 1534 | "actionTokenGeneratedByAdminLifespan" : "43200", 1535 | "_browser_header.contentSecurityPolicyReportOnly" : "", 1536 | "bruteForceProtected" : "false", 1537 | "_browser_header.contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", 1538 | "waitIncrementSeconds" : "60", 1539 | "offlineSessionMaxLifespanEnabled" : "false" 1540 | }, 1541 | "keycloakVersion" : "4.6.0.Final", 1542 | "userManagedAccessAllowed" : false 1543 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example.keycloak.demo 7 | spring-boot-keycloak-angular-parent-project 8 | 1.0.0 9 | pom 10 | 11 | spring-boot-keycloak-angular-parent-project 12 | Parent project for the Spring Boot and Angular and Keycloak security demo 13 | 14 | 15 | --------------------------------------------------------------------------------