Angular 7 + Spring Boot Application: Hello World Example

Angular 7 + Spring Boot Application: Hello World Example

<strong>In this tutorial, we will create a full-stack application where we expose an endpoint using Spring Boot and consume this endpoint using Angular 7 and display the data. In the next tutorial, we will be further enhancing this application and performing CRUD operations.</strong>

In this tutorial, we will create a full-stack application where we expose an endpoint using Spring Boot and consume this endpoint using Angular 7 and display the data. In the next tutorial, we will be further enhancing this application and performing CRUD operations.

Previously, we have seen what a PCF is and how to deploy an application to PCF. I have deployed this application we are developing to PCF. The demo application is as follows-

Angular 7 + Spring Boot Demo

What Is a Full-Stack Application?

In a full-stack application, we expose the backend point to get the data. This data can then be used by any application or device as per the developer’s need. In the future, even if another front-end device is to be used, there will not be much change and the new device will need to consume these endpoints.

The project architecture we will be developed as follows:

This tutorial is explained in the below Youtube Video -

https://youtu.be/CCOLXL-hOfQ

Spring Boot Application

We will be creating a simple Spring Boot Application to expose a REST endpoint to return a list of employees. In a previous tutorial, we had seen how to fetch the data using Spring Boot JDBC. However, for this tutorial, we will be mocking the list of employees to be returned. Maven Project will be as follows:The Maven will be as follows:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.javainuse</groupId>
<artifactId>SpringBootHelloWorld</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>SpringBootHelloWorld</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.1.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

Create the SpringBootHelloWorldApplication.java file as shown below:

package com.javainuse;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootHelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootHelloWorldApplication.class, args);
}
}

Create the Employee model class as follows

package com.javainuse.model;

public class Employee {
private String empId;
private String name;
private String designation;
private double salary;

public Employee() {
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getDesignation() {
return designation;
}

public void setDesignation(String designation) {
this.designation = designation;
}

public double getSalary() {
return salary;
}

public void setSalary(double salary) {
this.salary = salary;
}

public String getEmpId() {
return empId;
}

public void setEmpId(String empId) {
this.empId = empId;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((designation == null) ? 0 : designation.hashCode());
result = prime * result + ((empId == null) ? 0 : empId.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
long temp;
temp = Double.doubleToLongBits(salary);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Employee other = (Employee) obj;
if (designation == null) {
if (other.designation != null)
return false;
} else if (!designation.equals(other.designation))
return false;
if (empId == null) {
if (other.empId != null)
return false;
} else if (!empId.equals(other.empId))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (Double.doubleToLongBits(salary) != Double.doubleToLongBits(other.salary))
return false;
return true;
}
}

@RequestMapping maps the /employee request to return a list of employees. Also, here we are using the Cross-Origin annotation to specify that calls will be made to this controller from different domains. In our case, we have specified that a call can be made from localhost:4200.

package com.javainuse.controllers;

import java.util.ArrayList;
import java.util.List;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.javainuse.model.Employee;

@CrossOrigin(origins = "http://localhost:4200")
@RestController
public class TestController {

private List&lt;Employee&gt; employees = createList();

	@RequestMapping(value = "/employees", method = RequestMethod.GET, produces = "application/json")
	public List&lt;Employee&gt; firstPage() {
		return employees;
}

	private static List&lt;Employee&gt; createList() {
		List&lt;Employee&gt; tempEmployees = new ArrayList&lt;&gt;();
		Employee emp1 = new Employee();
		emp1.setName("emp1");
		emp1.setDesignation("manager");
		emp1.setEmpId("1");
		emp1.setSalary(3000);

		Employee emp2 = new Employee();
		emp2.setName("emp2");
		emp2.setDesignation("developer");
		emp2.setEmpId("2");
		emp2.setSalary(3000);
		tempEmployees.add(emp1);
		tempEmployees.add(emp2);
		return tempEmployees;
}

}

Compile and run SpringBootHelloWorldApplication.java as a Java application. Go to localhost:8080/employees.

Angular 7 DevelopmentInstalling Angular CLI
  • Install Node.js by downloading the installable from Install Node.js.* Install Angular CLI using the following command. It wll get us the latest version of Angular CKI.
npm install -g @angular/cli
  • We can check the Angular LI version, like so:
ng version
  • Next, we will create a new Angular project using the Angular CLI as follows:
ng new employee-management
  • *
  • To get the Angular CLI project started use the following command. We must go inside the employee-management folder and then use it.
ng serve

Go to localhost:4200 I will be using the Microsoft Visual Studio Code IDE for Angular. Let’s import the project we developed earlier in Microsoft Visual Studio Code IDE. Our final Angular project’s file structure will be as follows: 

TypeScript

TypeScript is a superset of JavaScript. It is a strongly typed language. So, unlike with JavaScript, we know if some syntax is wrong while typing it and rather than having to wait until runtime. In Angular, it is compiled to JavaScript while rendering the application in the browser.

Component

In Angular, we break complex code into reusable parts called components. A major part of development with Angular 7 is done in the components. Components are basically classes that interact with the .html file of the component, which gets displayed on the browser.

Service

In Angular, we might have scenarios where some code needs to be reused in multiple components. For example, a data connection that fetches data from a database might be needed in multiple components. This is achieved using services.

Create the Employee Component

We will be creating an employee component which will fetch data from Spring boot and display it. Lets begin with the employee component. Open your terminal and use the following command:

ng generate component employee

For this, Angular will have created the following four files:

  • employee.component.ts
  • employee.component.spec.ts
  • employee.component.html
  • employee.component.css

Next, in the app-routing.module.ts file, we will be defining the URL for accessing this component.

If we got o localhost:4200, we can see the following output 

Create HttpClient Service

Next, we will be creating an HTTPClient Service. This service will have the httpClient and will be responsible for calling HTTP GET requests to the backend Spring Boot application. In Angular, a service is written for any cross-cutting concerns and may be used by more than one components

ng generate service service/httpClient

The following service files are created:

  • http-client.service.ts
  • http-client.service.spec.ts

We will be modifying the http-client.service.ts file. In the constructor define the HTTPClient instance we will be using to make a call to the Spring Boot application. Here we will be using the Angular HTTPClient for calling the Spring Boot API to fetch the employee data. Also, we will be creating a method which makes calls to the Spring Boot application using the defined httpClient. Also, we need to add the HTTPClientModule to the app.module.ts

Insert HttpClient Service in Employee Component

Next, using the constructor dependency injection, we will be providing EmployeeComponen as an instance of HttpClientService. Using this service we make a call to the Spring Boot application to get a list of employees.

import { Component, OnInit } from '@angular/core';
import { HttpClientService } from '../service/http-client.service';

@Component({
selector: 'app-employee',
templateUrl: './employee.component.html',
styleUrls: ['./employee.component.css']
})
export class EmployeeComponent implements OnInit {

employees:string[];

constructor(
private httpClientService:HttpClientService
) { }

ngOnInit() {
this.httpClientService.getEmployees().subscribe(
response =>this.handleSuccessfulResponse(response),
);
}

handleSuccessfulResponse(response)
{
this.employees=response;
}

}

In the employee.component.html file, we iterate over the list of employees we got in the employee.component.ts file.

<table border="1">
<thead></thead>
<tr>
<th>name</th>
<th>designation</th>
</tr>

<tbody>
<tr *ngFor="let employee of employees">
<td>{{employee.name}}</td>
<td>{{employee.designation}}</td>
</tr>
</tbody>
</table>

Go to localhost:4200

And we’re done!

Originally published by Azlan Shaikh at https://dzone.com

Learn more

☞ Angular 7 (formerly Angular 2) - The Complete Guide

☞ The Complete Angular Course: Beginner to Advanced

☞ NativeScript + Angular: Build Native iOS, Android & Web Apps

☞ Build awesome web apps using Angular 7

☞ Go Full Stack with Spring Boot and Angular 7

☞ Angular 6 (Angular 2+) & React 16 - The Complete App Guide

☞ NgRx In Depth (Angular 7 and NgRx 7, with FREE E-Book)

☞ Angular & NodeJS - The MEAN Stack Guide

☞ Learn and Understand AngularJS

Angular file upload Spring Boot backend

Angular file upload Spring Boot backend

This tutorial shows a step by step solution to Angular file upload Spring Boot backend and store it on file system. We will cover a file upload from a basic input and also from a drag-drop area.

Server side

This section covers the Spring Boot server implementation

Spring Boot Controller

Our Spring Boot application exposes a single endpoint to upload files. Below it’s content :

package com.roufid.tutorials.controller;
 
import com.roufid.tutorials.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
 
import java.io.IOException;
 
@RestController
public class FileController {
 
	private final FileService fileService;
 
	@Autowired
	public FileController(FileService fileService) {
		this.fileService = fileService;
	}
 
	@PostMapping(value = "/api/files")
	@ResponseStatus(HttpStatus.OK)
	public void handleFileUpload(@RequestParam("file") MultipartFile file) throws IOException {
		fileService.storeFile(file);
	}
 
}

The controller is as simple as you see. Let’s see now the  FileService.

Spring Component FileService

We extracted the logic of storing the file a Spring Component FileService. Below it’s content :

package com.roufid.tutorials.service;
 
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
 
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
 
@Component
public class FileService {
 
	private static final String FILE_DIRECTORY = "/var/files";
 
	public void storeFile(MultipartFile file) throws IOException {
		Path filePath = Paths.get(FILE_DIRECTORY + "/" + file.getOriginalFilename());
 
		Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
	}
}

That’s it for server side. Let’s see now the front end implementation.

Client side with Angular

Angular CLI is used to create the Angular application along with  ng2-file-upload module to handle file upload from client side.

Angular Module

First, add the FileUploadModule to app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {FileUploadModule} from 'ng2-file-upload';
 
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
 
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FileUploadModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Angular component

Our main component contains a input of type “file” and a drag-and-drop area :

  • app.component.html
<h1>Roufid File upload tutorial</h1>
 
<div>
  <input #fileInput type="file" ng2FileSelect [uploader]="uploader"/>
 
  <div class="drop-box" ng2FileDrop
       [ngClass]="{'dragover': isDropOver}"
       [uploader]="uploader"
       (fileOver)="fileOverAnother($event)"
       (click)="fileClicked()">
    <span class="drag-in-title">Import or drag file here</span>
    <span class="drag-over-title">Drop the file</span>
  </div>
</div>
 
<router-outlet></router-outlet>

A CSS is added to make the look and feel pretty

  • app.component.scss
.drop-box {
  cursor: pointer;
  background: #F8F8F8;
  border: 5px dashed #DDD;
  text-align: center;
  padding: 40px;
}
 
.drag-over-title {
  display: none;
}
 
.drop-box.dragover {
  opacity: 0.6;
}
 
.drop-box.dragover .drag-over-title {
  display: block;
}
 
.drop-box.dragover .drag-in-title {
  display: none;
}
 
input[type='file'] {
  display: none;
}

And then creating the logic in the TypeScript component.

  • app.component.ts
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {FileUploader} from 'ng2-file-upload';
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
 
  @ViewChild('fileInput') fileInput: ElementRef;
 
  uploader: FileUploader;
  isDropOver: boolean;
 
  ngOnInit(): void {
    const headers = [{name: 'Accept', value: 'application/json'}];
    this.uploader = new FileUploader({url: 'api/files', autoUpload: true, headers: headers});
 
    this.uploader.onCompleteAll = () => alert('File uploaded');
  }
 
  fileOverAnother(e: any): void {
    this.isDropOver = e;
  }
 
  fileClicked() {
    this.fileInput.nativeElement.click();
  }
}

That’s is. The result looks like :Angular Spring boot file upload

Conclusion

I hope this article served you that you were looking for. If you have anything that you want to add or share then please share it below in the comment section.

Build Web App Authentication using Spring Boot, Spring Security, MongoDB and Angular 8

Build Web App Authentication using Spring Boot, Spring Security, MongoDB and Angular 8

In this article, you'll learn how to build web application authentication using Spring Boot, Spring Security, MongoDB, and Angular 8

In this tutorial, we have to build Spring Boot, Spring Security Core, and MongoDB RESTful Authentication as the backend. The Angular 8 used as the frontend using the HttpClient, HttpInterceptor, and RouteGuard modules. The secure page guarded by Angular 8 Route Guard and the secure RESTful API guarded by Spring Security REST. This should be a great combination of a secure modern web application.

The flow of the web application looks like below Sequence Diagram.

The Angular 8 application starts with the secure and route guarded page list of products without authorization headers. The Angular 8 redirect to the Login page. After login with the valid credential, the Angular 8 got the JWT token that validates with Route guard also send together with Authorization headers bearer. In the products list page, the Angular 8 application request products API to Spring Boot Security API include authorization headers. Finally, the Angular 8 page displaying the list of products.

The following tools, frameworks, libraries, and modules are required for this tutorial:

We assume that you already installed all required software, tools, and frameworks. So, we will not cover how to install that software, tools, and frameworks. Notes: We are using Spring Boot 2.1.6 (Stable version), JDK 1.8.0_92, and Angular 8.0.6.

Generate a New Spring Boot Gradle Project

To create or generate a new Spring Boot Application or Project, simply go to Spring Initializer. Fill all required fields as below then click on Generate Project button.

The project will automatically be downloaded as a Zip file. Next, extract the zipped project to your java projects folder. On the project folder root, you will find build.gradle file for register dependencies, initially it looks like this.

plugins {

    id 'org.springframework.boot' version '2.1.6.RELEASE'

    id 'java'

    id 'war'

}

 

apply plugin: 'io.spring.dependency-management'

 

group = 'com.djamware'

version = '0.0.1-SNAPSHOT'

sourceCompatibility = '1.8'

 

repositories {

    mavenCentral()

}

 

dependencies {

    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'

    implementation 'org.springframework.boot:spring-boot-starter-security'

    implementation 'org.springframework.boot:spring-boot-starter-web'

    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    testImplementation 'org.springframework.security:spring-security-test'

}

Now, you can work with the source code of this Spring Boot Project using your own IDE or Text Editor. We are using Visual Studio Code (VSCode). In the terminal or command line go to the extracted project folder then run the command to open VSCode.

cd spring-angular-auth 
code .

Next, we have to add the JWT library to the build.gradle as the dependency. Open and edit build.gradle then add this line to dependencies after other implementation.

implementation 'io.jsonwebtoken:jjwt:0.9.1'

Next, compile the Gradle Project by type this command from Terminal or CMD.

gradle compileJava

Next, open and edit src/main/resources/application.properties then add these lines.

spring.data.mongodb.database=springmongodb 
spring.data.mongodb.host=localhost 
spring.data.mongodb.port=27017

Create Product, User and Role Model or Entity Classes

We will be creating all required models or entities for products, user and role. Create a new folder and files inside that new folder.

mkdir src/main/java/com/djamware/springangularauth/models touch src/main/java/com/djamware/springangularauth/models/Products.java touch src/main/java/com/djamware/springangularauth/models/User.java touch src/main/java/com/djamware/springangularauth/models/Role.java

Next, open and edit src/main/java/com/djamware/springangularauth/models/Products.java then add these package and imports.

package com.djamware.springangularauth.models; 

import org.springframework.data.annotation.Id; 
import org.springframework.data.mongodb.core.mapping.Document;

Next, add these lines of codes for class, fields, document annotation that map to MongoDB collection, constructors, and getter/setter for each field.

@Document(collection = "products")
public class Products {
 
    @Id
    String id;
    String prodName;
    String prodDesc;
    Double prodPrice;
    String prodImage;
 
    public Products() {
    }
 
    public Products(String prodName, String prodDesc, Double prodPrice, String prodImage) {
        super();
        this.prodName = prodName;
        this.prodDesc = prodDesc;
        this.prodPrice = prodPrice;
        this.prodImage = prodImage;
    }
 
    public String getId() {
        return id;
    }
 
    public void setId(String id) {
        this.id = id;
    }
 
    public String getProdName() {
        return prodName;
    }
 
    public void setProdName(String prodName) {
        this.prodName = prodName;
    }
 
    public String getProdDesc() {
        return prodDesc;
    }
 
    public void setProdDesc(String prodDesc) {
        this.prodDesc = prodDesc;
    }
 
    public Double getProdPrice() {
        return prodPrice;
    }
 
    public void setProdPrice(Double prodPrice) {
        this.prodPrice = prodPrice;
    }
 
    public String getProdImage() {
        return prodImage;
    }
 
    public void setProdImage(String prodImage) {
        this.prodImage = prodImage;
    }
 
}

Next, open and edit src/main/java/com/djamware/springangularauth/models/User.java then add these package and imports.

package com.djamware.springangularauth.models;
 
import java.util.Set;
 
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.IndexDirection;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;

Add these lines of Java codes that contain document annotation that map to the MongoDB collection, class name, fields, constructors, and getter/setter of the fields.

@Document(collection = "users")
public class User {
 
    @Id
    private String id;
    @Indexed(unique = true, direction = IndexDirection.DESCENDING, dropDups = true)
    private String email;
    private String password;
    private String fullname;
    private boolean enabled;
    @DBRef
    private Set<Role> roles;
 
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getFullname() {
        return fullname;
    }
    public void setFullname(String fullname) {
        this.fullname = fullname;
    }
    public boolean isEnabled() {
        return enabled;
    }
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
    public Set<Role> getRoles() {
        return roles;
    }
    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }
 
}

Next, open and edit src/main/java/com/djamware/springangularauth/models/Role.java then add these package name and imports.

package com.djamware.springangularauth.models;
 
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.IndexDirection;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;

Add these lines of codes that contain document annotation that map to MongoDB collection, class name, fields/variables, constructors, and getter/setter for each field/variables.

@Document(collection = "users")
public class User {
 
    @Id
    private String id;
    @Indexed(unique = true, direction = IndexDirection.DESCENDING, dropDups = true)
    private String email;
    private String password;
    private String fullname;
    private boolean enabled;
    @DBRef
    private Set<Role> roles;
 
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getFullname() {
        return fullname;
    }
    public void setFullname(String fullname) {
        this.fullname = fullname;
    }
    public boolean isEnabled() {
        return enabled;
    }
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
    public Set<Role> getRoles() {
        return roles;
    }
    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }
 
}

If you want easier Java class creation, auto import organization, generate constructor, and generate getter/setter then you can use IDE like Eclipse, Netbeans, Spring Tool-Suite, or IntelliJ IDEA. So, you don't have to type all codes manually.

Create Product, User and Role Repository Interfaces

Next steps to create Product, User, and Role Repository Interfaces. Create a new folder and files for the repositories.

mkdir src/main/java/com/djamware/springangularauth/repositories
touch src/main/java/com/djamware/springangularauth/repositories/ProductRepository.java
touch src/main/java/com/djamware/springangularauth/repositories/UserRepository.java
touch src/main/java/com/djamware/springangularauth/repositories/RoleRepository.java

Next, open and edit src/main/java/com/djamware/springangularauth/repositories/ProductRepository.java then add these package name and imports.

package com.djamware.springangularauth.repositories;
 
import com.djamware.springangularauth.models.Products;
import org.springframework.data.mongodb.repository.MongoRepository;

Add these lines of codes that contain Java interface name that extends MongoRepository and deletes method.

public interface ProductRepository extends MongoRepository<Products, String> {
 
    @Override
    void delete(Products deleted);
}

Next, open and edit src/main/java/com/djamware/springangularauth/repositories/UserRepository.java then add these package name and imports.

package com.djamware.springangularauth.repositories; 

import com.djamware.springangularauth.models.User; 
import org.springframework.data.mongodb.repository.MongoRepository;

Add these lines of codes that contain Java interface name that extends MongoRepository and find by email method.

public interface UserRepository extends MongoRepository<User, String> {
 
    User findByEmail(String email);
}

Next, open and edit src/main/java/com/djamware/springangularauth/repositories/RoleRepository.java then add these package name and imports.

package com.djamware.springangularauth.repositories; 

import org.springframework.data.mongodb.repository.MongoRepository; 
import com.djamware.springangularauth.models.Role;

Add these lines of codes that contain Java interface name that extends MongoRepository and find by role method.

public interface RoleRepository extends MongoRepository<Role, String> {
 
    Role findByRole(String role);
}

Create a Custom User Details Service

To implements authentication using existing User and Role from MongoDB, we have to create a custom user details service. Create a new folder and Java file for the service.

mkdir src/main/java/com/djamware/springangularauth/services 
touch src/main/java/com/djamware/springangularauth/services/CustomUserDetailsService.java

Next, open and edit src/main/java/com/djamware/springangularauth/services/CustomUserDetailsService.java then add these package name and imports.

package com.djamware.springangularauth.services;
 
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
 
import com.djamware.springangularauth.models.Role;
import com.djamware.springangularauth.models.User;
import com.djamware.springangularauth.repositories.RoleRepository;
import com.djamware.springangularauth.repositories.UserRepository;

Add these lines of codes that contains service annotation, the class name that implements UserDetailsService, required variables, constructors, and methods for the authentication process.

@Service
public class CustomUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
 
    @Autowired
    private RoleRepository roleRepository;
 
    @Autowired
    private PasswordEncoder bCryptPasswordEncoder;
 
    public User findUserByEmail(String email) {
        return userRepository.findByEmail(email);
    }
 
    public void saveUser(User user) {
        user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
        user.setEnabled(true);
        Role userRole = roleRepository.findByRole("ADMIN");
        user.setRoles(new HashSet<>(Arrays.asList(userRole)));
        userRepository.save(user);
    }
 
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
 
        User user = userRepository.findByEmail(email);
        if(user != null) {
            List<GrantedAuthority> authorities = getUserAuthority(user.getRoles());
            return buildUserForAuthentication(user, authorities);
        } else {
            throw new UsernameNotFoundException("username not found");
        }
    }
 
    private List<GrantedAuthority> getUserAuthority(Set<Role> userRoles) {
        Set<GrantedAuthority> roles = new HashSet<>();
        userRoles.forEach((role) -> {
            roles.add(new SimpleGrantedAuthority(role.getRole()));
        });
 
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>(roles);
        return grantedAuthorities;
    }
 
    private UserDetails buildUserForAuthentication(User user, List<GrantedAuthority> authorities) {
        return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), authorities);
    }
}

Configure Spring Boot Security Rest

Now, the main purpose of this tutorial is configuring Spring Security Rest. First, we have to create a bean for JWT token generation and validation. Create a new folder and file for the configuration.

mkdir src/main/java/com/djamware/springangularauth/configs
touch src/main/java/com/djamware/springangularauth/configs/JwtTokenProvider.java
touch src/main/java/com/djamware/springangularauth/configs/JwtConfigurer.java
touch src/main/java/com/djamware/springangularauth/configs/JwtTokenFilter.java
touch src/main/java/com/djamware/springangularauth/configs/WebSecurityConfig.java

Next, open and edit src/main/java/com/djamware/springangularauth/configs/JwtTokenProvider.java then add these package name and imports.

package com.djamware.springangularauth.configs;
 
import java.util.Base64;
import java.util.Date;
import java.util.Set;
 
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
 
import com.djamware.springangularauth.models.Role;
import com.djamware.springangularauth.services.CustomUserDetailsService;
 
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

Next, add these lines of codes that contain component annotation, class name, variables, and methods.

@Component
public class JwtTokenProvider {
 
      @Value("${security.jwt.token.secret-key:secret}")
    private String secretKey = "secret";
 
    @Value("${security.jwt.token.expire-length:3600000}")
    private long validityInMilliseconds = 3600000; // 1h
 
    @Autowired
    private CustomUserDetailsService userDetailsService;
 
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }
 
    public String createToken(String username, Set<Role> set) {
        Claims claims = Jwts.claims().setSubject(username);
        claims.put("roles", set);
        Date now = new Date();
        Date validity = new Date(now.getTime() + validityInMilliseconds);
        return Jwts.builder()//
            .setClaims(claims)//
            .setIssuedAt(now)//
            .setExpiration(validity)//
            .signWith(SignatureAlgorithm.HS256, secretKey)//
            .compact();
    }
 
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = this.userDetailsService.loadUserByUsername(getUsername(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }
 
    public String getUsername(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }
 
    public String resolveToken(HttpServletRequest req) {
        String bearerToken = req.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7, bearerToken.length());
        }
        return null;
    }
 
    public boolean validateToken(String token) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            if (claims.getBody().getExpiration().before(new Date())) {
                return false;
            }
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            throw new JwtException("Expired or invalid JWT token");
        }
    }
}

Next, open and edit src/main/java/com/djamware/springangularauth/configs/JwtTokenFilter.java then add these package name and imports.

package com.djamware.springangularauth.configs;
 
import java.io.IOException;
 
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
 
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

Next, add these lines of codes that contain component annotation, class name, variables, and methods.

public class JwtTokenFilter extends GenericFilterBean {
 
    private JwtTokenProvider jwtTokenProvider;
 
    public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }
 
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
        throws IOException, ServletException {
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) req);
        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication auth = token != null ? jwtTokenProvider.getAuthentication(token) : null;
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.doFilter(req, res);
    }
}

Next, open and edit src/main/java/com/djamware/springangularauth/configs/JwtConfigurer.java then add these package name and imports.

package com.djamware.springangularauth.configs;
 
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

Next, add these lines of codes that contain component annotation, the class name that extends the SecurityConfigurerAdapter, variables, and methods.

public class JwtConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
 
    private JwtTokenProvider jwtTokenProvider;
 
    public JwtConfigurer(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }
 
    @Override
    public void configure(HttpSecurity http) throws Exception {
        JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider);
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

Finally, we have to configure the Spring Security by open and edit src/main/java/com/djamware/springangularauth/configs/WebSecurityConfig.java then add these package name and imports.

package com.djamware.springangularauth.configs;
 
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
 
import com.djamware.springangularauth.services.CustomUserDetailsService;

Add these lines of codes that contains Configuration and EnableWebSecurity annotation, class name that extends WebSecurityConfigurerAdapter, required variables, constructors, and methods.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Autowired
    JwtTokenProvider jwtTokenProvider;
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        UserDetailsService userDetailsService = mongoUserDetails();
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
 
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().disable().csrf().disable().sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
                .antMatchers("/api/auth/login").permitAll().antMatchers("/api/auth/register").permitAll()
                .antMatchers("/api/products/**").hasAuthority("ADMIN").anyRequest().authenticated().and().csrf()
                .disable().exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint()).and()
                .apply(new JwtConfigurer(jwtTokenProvider));
    http.cors();
    }
 
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**");
    }
 
    @Bean
    public PasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
 
    @Bean
    public AuthenticationEntryPoint unauthorizedEntryPoint() {
        return (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
                "Unauthorized");
    }
 
    @Bean
    public UserDetailsService mongoUserDetails() {
        return new CustomUserDetailsService();
    }
}

Create Product and Authentication Controllers

Now it's time for REST API endpoint. All RESTful API will be created from each controller. Product controller will handle CRUD endpoint of product and Authentication controller will handle login and register endpoint. Create a new folder and files for the controllers.

mkdir src/main/java/com/djamware/springangularauth/controllers
touch src/main/java/com/djamware/springangularauth/controllers/ProductController.java
touch src/main/java/com/djamware/springangularauth/controllers/AuthController.java
touch src/main/java/com/djamware/springangularauth/controllers/AuthBody.java

Next, open and edit src/main/java/com/djamware/springangularauth/controllers/AuthController.java then add these package name and imports.

package com.djamware.springangularauth.controllers;
 
import java.util.Optional;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
 
import com.djamware.springangularauth.models.Products;
import com.djamware.springangularauth.repositories.ProductRepository;

Add these lines of codes that contain RestController annotation, class name, required variables, constructors, and methods.

@CrossOrigin(origins = "*")
@RequestMapping("/api")
@RestController
public class ProductController {
 
    @Autowired
    ProductRepository productRepository;
 
    @RequestMapping(method = RequestMethod.GET, value = "/products")
    public Iterable<Products> product() {
        return productRepository.findAll();
    }
}

Next, open and edit src/main/java/com/djamware/springangularauth/controllers/AuthController.java then add these package name and imports.

package com.djamware.springangularauth.controllers;
 
import static org.springframework.http.ResponseEntity.ok;
 
import java.util.HashMap;
import java.util.Map;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import com.djamware.springangularauth.configs.JwtTokenProvider;
import com.djamware.springangularauth.models.User;
import com.djamware.springangularauth.repositories.UserRepository;
import com.djamware.springangularauth.services.CustomUserDetailsService;

Add these lines of codes that contain RestController and RequestMapping annotation, class name, required variables, constructors, and methods.

@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api/auth")
public class AuthController {
 
    @Autowired
    AuthenticationManager authenticationManager;
 
    @Autowired
    JwtTokenProvider jwtTokenProvider;
 
    @Autowired
    UserRepository users;
 
    @Autowired
    private CustomUserDetailsService userService;
 
    @SuppressWarnings("rawtypes")
    @PostMapping("/login")
    public ResponseEntity login(@RequestBody AuthBody data) {
        try {
            String username = data.getEmail();
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, data.getPassword()));
            String token = jwtTokenProvider.createToken(username, this.users.findByEmail(username).getRoles());
            Map<Object, Object> model = new HashMap<>();
            model.put("username", username);
            model.put("token", token);
            return ok(model);
        } catch (AuthenticationException e) {
            throw new BadCredentialsException("Invalid email/password supplied");
        }
    }
 
    @SuppressWarnings("rawtypes")
    @PostMapping("/register")
    public ResponseEntity register(@RequestBody User user) {
        User userExists = userService.findUserByEmail(user.getEmail());
        if (userExists != null) {
            throw new BadCredentialsException("User with username: " + user.getEmail() + " already exists");
        }
        userService.saveUser(user);
        Map<Object, Object> model = new HashMap<>();
        model.put("message", "User registered successfully");
        return ok(model);
    }
}

Next, open and edit src/main/java/com/djamware/springangularauth/controllers/AuthBody.java then add these package name.

package com.djamware.springangularauth.controllers;

Add these lines of codes that contains the class name, fields or variables, and getter/setter for those variables.

public class AuthBody {
 
    private String email;
    private String password;
 
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
 
}

Install or Update Angular 8 CLI and Create Application

Before installing the Angular 8 CLI, make sure you have installed Node.js https://nodejs.org and can open Node.js command prompt. Next, open the Node.js command prompt then type this command to install

Angular 8 CLI.

sudo npm install -g @angular/cli

Next, create an Angular 8 application by typing this command in the root of the Spring Boot application/project directory.

ng new client

Where client is the name of the Angular 8 application. You can specify your own name, we like to name it client because it's put inside Spring Boot Project directory. If there's a question, we fill them with Y and SCSS. Next, go to the newly created Angular 8 application.

cd client

Run the Angular 8 application for the first time.

ng serve

Now, go to localhost:4200 and you should see this page.

Add Routes for Navigation between Angular 8 Pages/Component

On the previous steps, we have to add Angular 8 Routes when answering the questions. Now, we just added the required pages for CRUD (Create, Read, Update, Delete) Supplier data. Type this commands to add the Angular 8 components or pages.

ng g component products
ng g component auth/login
ng g component auth/register

Open src/app/app.module.ts then you will see those components imported and declared in @NgModule declarations. Next, open and edit src/app/app-routing.module.ts then add these imports.

import { ProductsComponent } from './products/products.component';
import { LoginComponent } from './auth/login/login.component';
import { RegisterComponent } from './auth/register/register.component';

Add these arrays to the existing routes constant.

const routes: Routes = [
  {
    path: '',
    redirectTo: 'products',
    pathMatch: 'full'
  },
  {
    path: 'products',
    component: ProductsComponent,
    data: { title: 'List of Products' }
  },
  {
    path: 'login',
    component: LoginComponent,
    data: { title: 'Login' }
  },
  {
    path: 'register',
    component: RegisterComponent,
    data: { title: 'Register' }
  }
];

Open and edit src/app/app.component.html and you will see the existing router outlet. Next, modify this HTML page to fit the CRUD page.

<div class="container">
  <router-outlet></router-outlet>
</div>

Open and edit src/app/app.component.scss then replace all SCSS codes with this.

.container {
  padding: 20px;
}

Create a custom Angular 8 HttpInterceptor

Before creating a custom Angular 8 HttpInterceptor, create a folder with the name client/src/app/interceptors. Next, create a file for the custom Angular 8 HttpInterceptor with the name client/src/app/interceptors/token.interceptor.ts. Open and edit that file the add these imports.

import { Injectable } from '@angular/core';
import {
    HttpRequest,
    HttpHandler,
    HttpEvent,
    HttpInterceptor,
    HttpResponse,
    HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Router } from '@angular/router';

Create a class that implementing HttpInterceptor method.

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
 
}

Inject the required module to the constructor inside the class.

constructor(private router: Router) {}

Implement a custom Interceptor function.

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
 
    const token = localStorage.getItem('token');
    if (token) {
      request = request.clone({
        setHeaders: {
          'Authorization': 'Bearer ' + token
        }
      });
    }
    if (!request.headers.has('Content-Type')) {
      request = request.clone({
        setHeaders: {
          'content-type': 'application/json'
        }
      });
    }
    request = request.clone({
      headers: request.headers.set('Accept', 'application/json')
    });
    return next.handle(request).pipe(
      map((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse) {
          console.log('event--->>>', event);
        }
        return event;
      }),
      catchError((error: HttpErrorResponse) => {
        console.log(error);
        if (error.status === 401) {
          this.router.navigate(['login']);
        }
        if (error.status === 400) {
          alert(error.error);
        }
        return throwError(error);
      }));
}

Next, we have to register this custom HttpInterceptor and HttpClientModule. Open and edit client/src/app.module.ts then add these imports.

import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; 
import { TokenInterceptor } from './interceptors/token.interceptor';

Add HttpClientModule to the @NgModule imports array.

imports: [
  BrowserModule,
  AppRoutingModule,
  HttpClientModule
],

Add the Interceptor modules to the provider array of the @NgModule.

providers: [
  {
    provide: HTTP_INTERCEPTORS,
    useClass: TokenInterceptor,
    multi: true
  }
],

Now, the HTTP interceptor is ready to intercept any request to the API.

Create Services for Accessing Product and Authentication API

To access the Spring Boot RESTful API from Angular 8 application, we have to create services for that. Type these commands to generate the Angular 8 services from the client folder.

ng g service auth 
ng g service product

Next, open and edit client/src/app/auth.service.ts then add these imports.

import { HttpClient } from '@angular/common/http'; 
import { Observable, of } from 'rxjs'; 
import { catchError, tap } from 'rxjs/operators';

Declare a constant variable as Spring Boot RESTful API URL after the imports.

const apiUrl = 'http://192.168.0.7:8080/api/auth/';

Declare the variables before the constructor that will use by Angular 8 Route Guard.

isLoggedIn = false; 
redirectUrl: string;

Inject the HttpClient module inside the constructor.

constructor(private http: HttpClient) { }

Create all required functions for Login, Logout, Register, and helper functions.

login(data: any): Observable<any> {
  return this.http.post<any>(apiUrl + 'login', data)
    .pipe(
      tap(_ => this.isLoggedIn = true),
      catchError(this.handleError('login', []))
    );
}
 
logout(): Observable<any> {
  return this.http.get<any>(apiUrl + 'signout')
    .pipe(
      tap(_ => this.isLoggedIn = false),
      catchError(this.handleError('logout', []))
    );
}
 
register(data: any): Observable<any> {
  return this.http.post<any>(apiUrl + 'register', data)
    .pipe(
      tap(_ => this.log('login')),
      catchError(this.handleError('login', []))
    );
}
 
private handleError<T>(operation = 'operation', result?: T) {
  return (error: any): Observable<T> => {
 
    console.error(error); // log to console instead
    this.log(`${operation} failed: ${error.message}`);
 
    return of(result as T);
  };
}
 
private log(message: string) {
  console.log(message);
}

Next, create an object class that represents Product data client/src/app/products/product.ts then replace all file contents with these.

export class Product {
    productId: number;
    isbn: string;
    title: string;
    author: string;
    description: string;
    publisher: string;
    publishedYear: number;
    price: number;
}

Next, open and edit client/src/app/services/product.service.ts then replace all codes with this.

import { Injectable } from '@angular/core';
import { Product } from './products/product';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
 
const apiUrl = 'http://192.168.0.7:8080/api/products';
 
@Injectable({
  providedIn: 'root'
})
export class ProductService {
 
  constructor(private http: HttpClient) { }
 
  getProducts(): Observable<Product[]> {
    return this.http.get<Product[]>(apiUrl + 'Product')
      .pipe(
        tap(_ => this.log('fetched Products')),
        catchError(this.handleError('getProducts', []))
      );
  }
 
  private handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {
 
      console.error(error); // log to console instead
      this.log(`${operation} failed: ${error.message}`);
 
      return of(result as T);
    };
  }
 
  private log(message: string) {
    console.log(message);
  }
}

Display List of Product using Angular 8 Material

To display a list of products to the Angular 8 template. First, open and edit client/src/app/products/products.component.ts then add these imports.

import { Product } from './product'; 
import { ProductService } from '../product.service'; 
import { AuthService } from '../auth.service'; 
import { Router } from '@angular/router';

Next, inject the Product and Auth Services to the constructor.

constructor(private productService: ProductService, private authService: AuthService, private router: Router) { }

Declare these variables before the constructor.

data: Product[] = [];
displayedColumns: string[] = ['prodName', 'prodDesc', 'prodPrice'];
isLoadingResults = true;

Create a function for consuming or get a product list from the producing service.

getProducts(): void {
  this.productService.getProducts()
    .subscribe(products => {
      this.data = products;
      console.log(this.data);
      this.isLoadingResults = false;
    }, err => {
      console.log(err);
      this.isLoadingResults = false;
    });
}

Call this function from ngOnInit.

ngOnInit() {
  this.getProducts();
}

Add a function for log out the current session.

logout() {
  localStorage.removeItem('token');
  this.router.navigate(['login']);
}

Next, for the user interface (UI) we will use Angular 8 Material and CDK. There's a CLI for generating a Material component like Table as a component, but we will create or add the Table component from scratch to existing component. Type this command to install Angular 8 Material.

ng add @angular/material

If there are some questions, answer them like below.

? Choose a prebuilt theme name, or "custom" for a custom theme: Purple/Green       [ Preview: https://material.angular.i
o?theme=purple-green ]
? Set up HammerJS for gesture recognition? Yes
? Set up browser animations for Angular Material? Yes

Next, we have to register all required Angular Material components or modules to src/app/app.module.ts. Open and edit that file then add this imports.

import {
  MatInputModule,
  MatPaginatorModule,
  MatProgressSpinnerModule,
  MatSortModule,
  MatTableModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatFormFieldModule } from '@angular/material';

Also, add FormsModule and ReactiveFormsModule.

import { FormsModule, ReactiveFormsModule } from '@angular/forms';

Register the above modules to @NgModule imports.

imports: [
  BrowserModule,
  FormsModule,
  HttpClientModule,
  AppRoutingModule,
  ReactiveFormsModule,
  BrowserAnimationsModule,
  MatInputModule,
  MatTableModule,
  MatPaginatorModule,
  MatSortModule,
  MatProgressSpinnerModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatFormFieldModule
],

Next, open and edit client/src/app/products/products.component.html then replace all HTML tags with this Angular 8 Material tags.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <div class="button-row">
    <a mat-flat-button color="primary" (click)="logout()">Logout</a>
  </div>
  <div class="mat-elevation-z8">
    <table mat-table [dataSource]="data" class="example-table">
 
      <!-- Product ID Column -->
      <ng-container matColumnDef="prodName">
        <th mat-header-cell *matHeaderCellDef>Product Name</th>
        <td mat-cell *matCellDef="let row">{{row.prodName}}</td>
      </ng-container>
 
      <!-- ISBN Column -->
      <ng-container matColumnDef="prodDesc">
        <th mat-header-cell *matHeaderCellDef>Product Description</th>
        <td mat-cell *matCellDef="let row">{{row.prodDesc}}</td>
      </ng-container>
 
      <!-- Title Column -->
      <ng-container matColumnDef="prodPrice">
        <th mat-header-cell *matHeaderCellDef>Product Price</th>
        <td mat-cell *matCellDef="let row">{{row.prodPrice}}</td>
      </ng-container>
 
      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
    </table>
  </div>
</div>

Finally, we have to align the style for this page. Open and edit client/src/app/products/products.component.scss then replace all SCSS codes with these.

/* Structure */
.example-container {
    position: relative;
    padding: 5px;
}
 
.example-table-container {
    position: relative;
    max-height: 400px;
    overflow: auto;
}
 
table {
    width: 100%;
}
 
.example-loading-shade {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 56px;
    right: 0;
    background: rgba(0, 0, 0, 0.15);
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
}
 
.example-rate-limit-reached {
    color: #980000;
    max-width: 360px;
    text-align: center;
}
 
/* Column Widths */
.mat-column-number,
.mat-column-state {
    max-width: 64px;
}
 
.mat-column-created {
    max-width: 124px;
}
 
.mat-flat-button {
    margin: 5px;
}

Create the Angular 8 Login and Register Page

This time for authentication part. Open and edit client/src/app/auth/login/login.component.ts then add these imports.

import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { AuthService } from '../../auth.service';
import { Router } from '@angular/router';
import { ErrorStateMatcher } from '@angular/material/core';

Declare these variables before the constructor.

loginForm: FormGroup;
email = '';
password = '';
matcher = new MyErrorStateMatcher();
isLoadingResults = false;

Inject the imported modules to the constructor.

constructor(private formBuilder: FormBuilder, private router: Router, private authService: AuthService) { }

Initialize NgForm to the NgOnInit function.

ngOnInit() {
  this.loginForm = this.formBuilder.group({
    'email' : [null, Validators.required],
    'password' : [null, Validators.required]
  });
}

Add a function to submit the login form.

onFormSubmit(form: NgForm) {
  this.authService.login(form)
    .subscribe(res => {
      console.log(res);
      if (res.token) {
        localStorage.setItem('token', res.token);
        this.router.navigate(['products']);
      }
    }, (err) => {
      console.log(err);
    });
}

Add a function to go to the Register page.

register() {
  this.router.navigate(['register']);
}

Add a class that handles the form validation above this class.

/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}

Next, open and edit client/src/app/auth/login/login.component.html then replace all HTML tags with these.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="loginForm" (ngSubmit)="onFormSubmit(loginForm.value)">
      <mat-form-field class="example-full-width">
        <input matInput type="email" placeholder="Email" formControlName="email"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!loginForm.get('email').valid && loginForm.get('email').touched">Please enter your email</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput type="password" placeholder="Password" formControlName="password"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!loginForm.get('password').valid && loginForm.get('password').touched">Please enter your password</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!loginForm.valid" mat-flat-button color="primary">Login</button>
      </div>
      <div class="button-row">
        <button type="button" mat-flat-button color="primary" (click)="register()">Register</button>
      </div>
    </form>
  </mat-card>
</div>

Next, give this page a style by open and edit client/src/app/auth/login/login.component.scss then applies these styles codes.

/* Structure */
.example-container {
  position: relative;
  padding: 5px;
}
 
.example-form {
  min-width: 150px;
  max-width: 500px;
  width: 100%;
}
 
.example-full-width {
  width: 100%;
}
 
.example-full-width:nth-last-child() {
  margin-bottom: 10px;
}
 
.button-row {
  margin: 10px 0;
}
 
.mat-flat-button {
  margin: 5px;
}

Next, for register page, open and edit client/src/app/auth/register/register.component.ts then replace all Typescript codes with these.

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { AuthService } from '../../auth.service';
import { Router } from '@angular/router';
import { ErrorStateMatcher } from '@angular/material/core';
 
@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.scss']
})
export class RegisterComponent implements OnInit {
 
  registerForm: FormGroup;
  fullName = '';
  email = '';
  password = '';
  isLoadingResults = false;
  matcher = new MyErrorStateMatcher();
 
  constructor(private formBuilder: FormBuilder, private router: Router, private authService: AuthService) { }
 
  ngOnInit() {
    this.registerForm = this.formBuilder.group({
      'fullName' : [null, Validators.required],
      'email' : [null, Validators.required],
      'password' : [null, Validators.required]
    });
  }
 
  onFormSubmit(form: NgForm) {
    this.authService.register(form)
      .subscribe(res => {
        this.router.navigate(['login']);
      }, (err) => {
        console.log(err);
        alert(err.error);
      });
  }
 
}
 
/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}

Next, open and edit client/src/app/auth/register/register.component.html then replace all HTML tags with these.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="registerForm" (ngSubmit)="onFormSubmit(registerForm.value)">
      <mat-form-field class="example-full-width">
        <input matInput type="fullName" placeholder="Full Name" formControlName="fullName"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!registerForm.get('fullName').valid && registerForm.get('fullName').touched">Please enter your Full Name</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput type="email" placeholder="Email" formControlName="email"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!registerForm.get('email').valid && registerForm.get('email').touched">Please enter your email</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput type="password" placeholder="Password" formControlName="password"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!registerForm.get('password').valid && registerForm.get('password').touched">Please enter your password</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!registerForm.valid" mat-flat-button color="primary">Register</button>
      </div>
    </form>
  </mat-card>
</div>

Finally, open and edit client/src/app/auth/register/register.component.scss then replace all SCSS codes with these.

/* Structure */
.example-container {
  position: relative;
  padding: 5px;
}
 
.example-form {
  min-width: 150px;
  max-width: 500px;
  width: 100%;
}
 
.example-full-width {
  width: 100%;
}
 
.example-full-width:nth-last-child() {
  margin-bottom: 10px;
}
 
.button-row {
  margin: 10px 0;
}
 
.mat-flat-button {
  margin: 5px;
}

Secure the Guarded Products Page using Angular 8 Route Guard

As we mention in the beginning that the Angular 8 application will use Angular 8 Route Guard to secure the products page. So, we have both securities for the Angular 8 page and for Spring Boot RESTful API. Type this command to generate a guard configuration file.

ng generate guard auth/auth

Open and edit that file then add this Angular 8 or Typescript imports.

import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../auth.service';

Next, add this implements code to the Class name.

export class AuthGuard implements CanActivate

Inject the AuthService and the Router to the constructor params.

constructor(private authService: AuthService, private router: Router) {}

Add the function for the Route Guard.

canActivate(
  next: ActivatedRouteSnapshot,
  state: RouterStateSnapshot): boolean {
  const url: string = state.url;
 
  return this.checkLogin(url);
}

Add the function to check the login status and redirect to the login page if it's not logged in and redirect to the Guarded page if it's logged in.

checkLogin(url: string): boolean {
  if (this.authService.isLoggedIn) { return true; }
 
  // Store the attempted URL for redirecting
  this.authService.redirectUrl = url;
 
  // Navigate to the login page with extras
  this.router.navigate(['/login']);
  return false;
}

Next, open and edit src/app/app-routing.module.ts then add this import.

import { AuthGuard } from './auth/auth.guard';

Modify the product path, so it will look like this.

const routes: Routes = [
  {
    path: 'products',
    canActivate: [AuthGuard],
    component: ProductsComponent,
    data: { title: 'List of Products' }
  },
  {
    path: 'login',
    component: LoginComponent,
    data: { title: 'Login' }
  },
  {
    path: 'register',
    component: RegisterComponent,
    data: { title: 'Register' }
  }
];

Run and Test The Authentication of The Spring Boot, Security, MongoDB, and Angular 8 Web Application

Before run the Spring Boot RESTful API, make sure the MongoDB server is running by type this command in another terminal or command line tab.

mongod

In the different tab run the Spring Boot RESTful API using this command.

gradle bootRun

Open again a new terminal tab then go to the client folder then run the Angular 8 application.

ng serve

Next, open the browser then go to [http://localhost:4200](http://localhost:4200 "http://localhost:4200") and you

should see then login page because of the landing page that point to

products page will redirect to login if not logged in.

That it's the Spring Boot, Security, MongoDB, Angular 8: Build Authentication. You can find the full source code from our GitHub.

How to Build Mobile Apps with Angular, Ionic 4, and Spring Boot

How to Build Mobile Apps with Angular, Ionic 4, and Spring Boot

Run Your Ionic App on Android. Make sure you're using Java 8. Run ionic cordova prepare android. Open platforms/android in Android Studio, upgrade Gradle if prompted. Set launchMode to singleTask in AndroidManifest.xml. Start your app using Android Studio...

In this brief tutorial, I’ll show you to use Ionic for JHipster v4 with Spring Boot and JHipster 6.

To complete this tutorial, you’ll need to have Java 8+, Node.js 10+, and Docker installed. You’ll also need to create an Okta developer account.

Create a Spring Boot + Angular App with JHipster

You can install JHipster via Homebrew (brew install jhipster) or with npm.

npm i -g [email protected]

Once you have JHipster installed, you have two choices. There’s the quick way to generate an app (which I recommend), and there’s the tedious way of picking all your options. I don’t care which one you use, but you must select OAuth 2.0 / OIDCauthentication to complete this tutorial successfully.

Here’s the easy way:

mkdir app && cd app

echo "application { config { baseName oauth2, authenticationType oauth2, \
  buildTool gradle, testFrameworks [protractor] }}" >> app.jh

jhipster import-jdl app.jh

The hard way is you run jhipster and answer a number of questions. There are so many choices when you run this option that you might question your sanity. At last count, I remember reading that JHipster allows 26K+ combinations!

The project generation process will take a couple of minutes to complete if you’re on fast internet and have a bad-ass laptop. When it’s finished, you should see output like the following.

OIDC with Keycloak and Spring Security

JHipster has several authentication options: JWT, OAuth 2.0 / OIDC, and UAA. With JWT (the default), you store the access token on the client (in local storage). This works but isn’t the most secure. UAA involves using your own OAuth 2.0 authorization server (powered by Spring Security), and OAuth 2.0 / OIDC allows you to use Keycloak or Okta.

Spring Security makes Keycloak and Okta integration so incredibly easy it’s silly. Keycloak and Okta are called "identity providers" and if you have a similar solution that is OIDC-compliant, I’m confident it’ll work with Spring Security and JHipster.

Having Keycloak set by default is nice because you can use it without having an internet connection.

To log into the JHipster app you just created, you’ll need to have Keycloak up and running. When you create a JHipster project with OIDC for authentication, it creates a Docker container definition that has the default users and roles. Start Keycloak using the following command.

docker-compose -f src/main/docker/keycloak.yml up -d

Start your application with ./gradlew (or ./mvnw if you chose Maven) and you should be able to log in using "admin/admin" for your credentials.

Open another terminal and prove all the end-to-end tests pass:

npm run e2e

If your environment is setup correctly, you’ll see output like the following:

> [email protected] e2e /Users/mraible/app
> protractor src/test/javascript/protractor.conf.js

[16:02:18] W/configParser - pattern ./e2e/entities/**/*.spec.ts did not match any files.
[16:02:18] I/launcher - Running 1 instances of WebDriver
[16:02:18] I/direct - Using ChromeDriver directly...


  account
    ✓ should fail to login with bad password
    ✓ should login successfully with admin account (1754ms)

  administration
    ✓ should load metrics
    ✓ should load health
    ✓ should load configuration
    ✓ should load audits
    ✓ should load logs


  7 passing (15s)

[16:02:36] I/launcher - 0 instance(s) of WebDriver still running
[16:02:36] I/launcher - chrome #01 passed
Execution time: 19 s.

OIDC with Okta and Spring Security

To switch to Okta, you’ll first need to create an OIDC app. If you don’t have an Okta Developer account, now is the time!

Log in to your Okta Developer account.

  • In the top menu, click on Applications
  • Click on Add Application
  • Select Web and click Next
  • Enter JHipster FTW! for the Name (this value doesn’t matter, so feel free to change it)
  • Change the Login redirect URI to be <a href="http://localhost:8080/login/oauth2/code/oidc" target="_blank">http://localhost:8080/login/oauth2/code/oidc</a>
  • Click Done, then Edit and add <a href="http://localhost:8080" target="_blank">http://localhost:8080</a> as a Logout redirect URI
  • Click Save

These are the steps you’ll need to complete for JHipster. Start your JHipster app using a command like the following:

SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI=https://{yourOktaDomain}/oauth2/default \
  SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID=$clientId \
  SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET=$clientSecret ./gradlew

Create a Native App for Ionic

You’ll also need to create a Native app for Ionic. The reason for this is because Ionic for JHipster is configured to use PKCE(Proof Key for Code Exchange). The current Spring Security OIDC support in JHipster still requires a client secret. PKCE does not.

Go back to the Okta developer console and follow the steps below:

  • In the top menu, click on Applications
  • Click on Add Application
  • Select Native and click Next
  • Enter Ionic FTW! for the Name
  • Add Login redirect URIs: <a href="http://localhost:8100/implicit/callback" target="_blank">http://localhost:8100/implicit/callback</a> and dev.localhost.ionic:/callback
  • Click Done, then Edit and add Logout redirect URIs: <a href="http://localhost:8100/implicit/logout" target="_blank">http://localhost:8100/implicit/logout</a> and dev.localhost.ionic:/logout
  • Click Save

You’ll need the client ID from your Native app, so keep your browser tab open or copy/paste it somewhere.

Create Groups and Add Them as Claims to the ID Token

In order to login to your JHipster app, you’ll need to adjust your Okta authorization server to include a groups claim.

On Okta, navigate to Users > Groups. Create ROLE_ADMIN and ROLE_USER groups and add your account to them.

Navigate to API > Authorization Servers, click the Authorization Servers tab and edit the default one. Click the Claims tab and Add Claim. Name it "groups" or "roles" and include it in the ID Token. Set the value type to "Groups" and set the filter to be a Regex of .*. Click Create.

Navigate to <a href="http://localhost:8080" target="_blank">http://localhost:8080</a>, click sign in and you’ll be redirected to Okta to log in.

Enter the credentials you used to signup for your account, and you should be redirected back to your JHipster app.

Generate Entities for a Photo Gallery

Let’s enhance this example a bit and create a photo gallery that you can upload pictures to. Kinda like Flickr, but waaayyyy more primitive.

JHipster has a JDL (JHipster Domain Language) feature that allows you to model the data in your app, and generate entities from it. You can use its JDL Studio feature to do this online and save it locally once you’ve finished.

I created a data model for this app that has an Album, Photo, and Tag entities and set up relationships between them. Below is a screenshot of what it looks like in JDL Studio.

Copy the JDL below and save it in a photos.jdl file in the root directory of your project.

entity Album {
  title String required,
  description TextBlob,
  created Instant
}

entity Photo {
  title String required,
  description TextBlob,
  image ImageBlob required,
  taken Instant
}

entity Tag {
  name String required minlength(2)
}

relationship ManyToOne {
  Album{user(login)} to User,
  Photo{album(title)} to Album
}

relationship ManyToMany {
  Photo{tag(name)} to Tag{photo}
}

paginate Album with pagination
paginate Photo, Tag with infinite-scroll

You can generate entities and CRUD code (Java for Spring Boot; TypeScript and HTML for Angular) using the following command:

jhipster import-jdl photos.jdl

When prompted, type a to update existing files.

This process will create Liquibase changelog files (to create your database tables), entities, repositories, Spring MVC controllers, and all the Angular code that’s necessary to create, read, update, and delete your data objects. It’ll even generate Jest unit tests and Protractor end-to-end tests!

When the process completes, restart your app, and confirm that all your entities exist (and work) under the Entities menu.

You might notice that the entity list screen is pre-loaded with data. This is done by faker.js. To turn it off, edit src/main/resources/config/application-dev.yml, search for liquibase and set its contexts value to dev. I made this change in this example’s code and ran ./gradlew clean to clear the database.

liquibase:
  # Add 'faker' if you want the sample data to be loaded automatically
  contexts: dev

Develop a Mobile App with Ionic and Angular

Getting started with Ionic for JHipster is similar to JHipster. You simply have to install the Ionic CLI, Yeoman, the module itself, and run a command to create the app.

npm i -g [email protected] [email protected] yo
yo jhipster-ionic

If you have your app application at ~/app, you should run this command from your home directory (~). Ionic for JHipster will prompt you for the location of your backend application. Use mobile for your app’s name and app for the JHipster app’s location.

Type a when prompted to overwrite mobile/src/app/app.component.ts.

Open mobile/src/app/auth/auth.service.ts in an editor, search for data.clientId and replace it with the client ID from your Native app on Okta.

// try to get the oauth settings from the server
this.requestor.xhr({method: 'GET', url: AUTH_CONFIG_URI}).then(async (data: any) => {
  this.authConfig = {
    identity_client: '{yourClientId}',
    identity_server: data.issuer,
    redirect_url: redirectUri,
    end_session_redirect_url: logoutRedirectUri,
    scopes,
    usePkce: true
  };
  ...
}

When using Keycloak, this change is not necessary.### Add Claims to Access Token

In order to authentication successfully with your Ionic app, you have to do a bit more configuration in Okta. Since the Ionic client will only send an access token to JHipster, you need to 1) add a groups claim to the access token and 2) add a couple more claims so the user’s name will be available in JHipster.

Navigate to API > Authorization Servers, click the Authorization Servers tab and edit the default one. Click the Claims tab and Add Claim. Name it "groups" and include it in the Access Token. Set the value type to "Groups" and set the filter to be a Regex of .*. Click Create.

Add another claim, name it given_name, include it in the access token, use Expression in the value type, and set the value to user.firstName. Optionally, include it in the profile scope. Perform the same actions to create a family_name claim and use expression user.lastName.

When you are finished, your claims should look as follows.

Run the following commands to start your Ionic app.

cd mobile
ionic serve

You’ll see a screen with a sign-in button. Click on it, and you’ll be redirected to Okta to authenticate.

Now that you having log in working, you can use the entity generator to generate Ionic pages for your data model. Run the following commands (in your ~/mobile directory) to generate screens for your entities.

yo jhipster-ionic:entity album

When prompted to generate this entity from an existing one, type Y. Enter ../app as the path to your existing application. When prompted to regenerate entities and overwrite files, type Y. Enter a when asked about conflicting files.

Go back to your browser where your Ionic app is running (or restart it if you stopped it). Click on Entities on the bottom, then Albums. Click the blue + icon in the bottom corner, and add a new album.

Click the ✔️ in the top right corner to save your album. You’ll see a success message and it listed on the next screen.

Refresh your JHipster app’s album list and you’ll see it there too!

Generate code for the other entities using the following commands and the same answers as above.

yo jhipster-ionic:entity photo
yo jhipster-ionic:entity tag

Run Your Ionic App on iOS

To generate an iOS project for your Ionic application, run the following command:

ionic cordova prepare ios

When prompted to install the ios platform, type Y. When the process completes, open your project in Xcode:

open platforms/ios/MyApp.xcworkspace

You’ll need to configure code signing in the General tab, then you should be able to run your app in Simulator.

Log in to your Ionic app, tap Entities and view the list of photos.

Add a photo in the JHipster app at <a href="http://localhost:8080" target="_blank">http://localhost:8080</a>.

To see this new album in your Ionic app, pull down with your mouse to simulate the pull-to-refresh gesture on a phone. Looky there - it works!

There are some gestures you should know about on this screen. Clicking on the row will take you to a view screen where you can see the photo’s details. You can also swipe left to expose edit and delete buttons.

Run Your Ionic App on Android

Deploying your app on Android is very similar to iOS. In short:

  1. Make sure you’re using Java 8
  2. Run ionic cordova prepare android
  3. Open platforms/android in Android Studio, upgrade Gradle if prompted
  4. Set launchMode to singleTask in AndroidManifest.xml
  5. Start your app using Android Studio
  6. While your app is starting, run adb reverse tcp:8080 tcp:8080 so the emulator can talk to JHipster
Learn More About Ionic 4 and JHipster 6

Ionic is a nice way to leverage your web development skills to build mobile apps. You can do most of your development in the browser, and deploy to your device when you’re ready to test it. You can also just deploy your app as a PWA and not both to deploy it to an app store.

JHipster supports PWAs too, but I think Ionic apps look like native apps, which is a nice effect. There’s a lot more I could cover about JHipster and Ionic, but this should be enough to get you started.

You can find the source code for the application developed in this post on GitHub at @oktadeveloper/okta-ionic4-jhipster-example.

Thank you for reading!