Romolo  Morelli

Romolo Morelli

1675321821

CRUD App with Vue.js and Spring Boot

In this tutorial, you'll how to create a CRUD (create, read, update, and delete) application using Spring Boot and Vue.js.

Table of Contents:

00:00 - Hello, developers!
01:07 - What I will build
01:28 - Install prerequisites
03:15 - Create an OpenID Connect app
05:14 - Bootstrap a Spring Boot app using Spring Initializr
06:21 - Configure Spring Security
09:33 - Test your Spring Boot API
10:06 - Secure your Spring Boot API
13:40 - Create a Vue JavaScript client
21:05 - Confirm your Spring Boot and Vue todo app works
22:36 - Test your API with an Access Token
23:50 - Find all the code on GitHub
24:33 - Thanks for watching!

Blog post: https://morioh.com/p/2ee0cfbf0383 
GitHub repo: https://github.com/oktadev/okta-spring-boot-vue-crud-example 
Demo script: https://github.com/oktadev/okta-spring-boot-vue-crud-example/blob/main/demo.adoc 

#vue #vuejs #springboot #java 

CRUD App with Vue.js and Spring Boot
Zachary Palmer

Zachary Palmer

1675321129

Build a Simple CRUD App with Spring Boot and Vue.js

Create a CRUD (create, read, update, and delete) application using Spring Boot and Vue.js. You'll use Vue and Spring Boot to build a todo list web application. 

You will use Vue and Spring Boot to build a todo list web application. The application will include CRUD abilities, meaning that you can create, read, update, and delete the todo items on the Spring Boot API via the client. The Vue frontend client will use the Quasar framework for the presentation. OAuth 2.0 and OpenID Connect (OIDC) will secure the Spring Boot API and the Vue client, initially by using Okta as the security provider. Then, at the end of the tutorial, you will also see how to use Auth0 as the security provider.

Spring Boot, Vue, and Okta logos

This project has two major parts:

  • Spring Boot API
  • Vue client

The Spring Boot app will include an H2 in-memory database and will use Spring Data JPA to map our todo data model to a database table for persistence. As you’ll see, the server will leverage Spring Boot’s ability to quickly expose data via a REST API with minimal configuration.

The client will use Vue 3 and the Quasar framework. The Quasar framework provides components and layout tools to help build Vue applications quickly with a consistent, high-quality user interface.

Before you dig into the tutorial, I want to quickly introduce the technologies for those that might be unfamiliar. Feel free to skip down to the prerequisites section if you’re already familiar with Vue and Spring Boot.

If you’re more of a visual learner, this tutorial is also available as a screencast.

 

Table of contents

What is Vue.js?

Vue is a JavaScript view library, like React and Angular. It’s designed to be incrementally adoptable, and the core library focuses solely on the view layer.

In my experience, Vue.js is a great alternative to React. I learned React first and came to use Vue later. Like React, Vue uses a virtual DOM, provides reactive and composable view components, and enforces a strict one-way parent-child relationship when defining properties and state. This means that it is performant and avoids many confusing state relationships that can occur without one-way data binding.

However, unlike React, Vue uses templates instead of JSX (a potentially welcome and more immediately accessible option). Vue gives you component-scoped CSS using style tags in single-file components. In practice, this difference is pretty significant because, in React, the JSX and CSS-like syntax are close enough to HTML and CSS to be confusing but not the same, which creates problems initially. (Ever gone from a language that doesn’t require semicolons back to one that does? It’s something like that.)

I find Vue to be a simpler, cleaner implementation. React requires a deep dive. You gotta take the red pill and go all the way. It’s a super powerful system, but you have to be all in. Vue is a little friendlier and a little easier to get started.

Introducing the Quasar Framework

The Quasar Framework builds on top of Vue to add a cross-platform component library and grid layout system. It also provides many tools for deploying Vue-based applications to basically any platform you can think of, from web single-page and progressive web apps to mobile apps and Electron-based desktop apps. In this tutorial, you’ll only be using the layout and component library features. Still, Quasar’s big push is to allow developers to write a single web application and deploy it to any platform with a consistent look with minimal changes.

About Spring Boot

The server technology you’re going to use is Spring Boot. Pure, unadulterated Spring (pre-Spring Boot) is a bit of a behemoth: super powerful but potentially time-sucking and frustrating. I’m pretty sure the whole computer conference phenomena came about so that people could learn and understand old-school Spring XML files. It certainly drove large sections of the computer publishing empires.

Spring Boot was Spring’s answer to this complexity (and to frameworks like Ruby on Rails and Grails). They did a great job of distilling down all the power of Spring into a simple, quick, easy-to-use web framework. You can have a fully functioning resource server with a ridiculously small number of lines of code and a few annotations.

Plus, when you’re ready, you have all the power of Spring under the hood, just waiting.

Prerequisites:

Before you start, please make sure you have the following prerequisites installed (or install them now).

  • Java 17: or use SDKMAN! to manage and install multiple versions
  • Okta CLI: the Okta command-line interface
  • HTTPie: a simple tool for making HTTP requests from a Bash shell
  • Node 16+
  • Vue CLI: you’ll use this to bootstrap the Vue client

You will need a free Okta Developer account if you don’t already have one. But you can wait until later in the tutorial and use the Okta CLI to log in or register for a new account.

Instead of building the project, you can also clone the repo and follow the instructions there to configure it.

Create an OpenID Connect app

Open a Bash shell. Create a parent directory for the project. Eventually, this will include both the resource server and client projects.

mkdir spring-boot-vue-crud
cd spring-boot-vue-crud

Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register to sign up for a new account. If you already have an account, run okta login. Then, run okta apps create. Select the default app name, or change it as you see fit. Choose Single-Page App and press Enter.

Use http://localhost:8080/callback for the Redirect URI and accept the default Logout Redirect URI of http://localhost:8080.

What does the Okta CLI do?

The Okta CLI will create an OIDC Single-Page App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. It will also add a trusted origin for http://localhost:8080. You will see output like the following when it’s finished:

Okta application configuration:
Issuer:    https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6

NOTE: You can also use the Okta Admin Console to create your app. See Create a Vue App for more information.

Copy the client ID and issuer URI somewhere safe. You’ll need them for both the client and resource server applications.

TIP: You can also use Auth0 to secure Spring Boot and the Vue client.

Bootstrap a Spring Boot app using Spring Initializr

You’re going to use the Spring Initializr to create a starter project for the resource server. You can look at the project website if you want, but here you’ll use the REST API to download a pre-configured starter.

The following command will download the starter project and un-tar it to a new directory named resource-server.

curl https://start.spring.io/starter.tgz \
  -d bootVersion=3.0.2 \
  -d javaVersion=17 \
  -d dependencies=web,data-rest,lombok,data-jpa,h2,okta \
  -d type=gradle-project \
  -d baseDir=resource-server \
| tar -xzvf - && cd resource-server

The dependencies you’re including are:

  • web: Spring Web MVC, adds basic HTTP REST functionality
  • data-jpa: Spring Data JPA, makes it easy to create JPA-based repositories
  • data-rest: Spring Data REST, exposes Spring Data repositories as resource servers
  • h2: the H2 in-memory database used for demonstration purposes
  • lombok: Project Lombok, adds some helpful annotations that eliminate the need to write a lot of getters and setters
  • okta: Okta Spring Boot Starter that helps OAuth 2.0 and OIDC configuration

Project Lombok saves a lot of clutter and ceremony code. However, if you’re using an IDE, you’ll need to install a plugin for Lombok. See the project’s installation docs for more information.

Configure Spring Security

Open the application properties file and update it. You’re changing the server port so it doesn’t conflict with the default Vue local server (which also defaults 8080).

src/main/resources/application.properties

server.port=9000
okta.oauth2.issuer=<your-issuer-uri>
okta.oauth2.clientId=<your-client-id>

You need to replace the two bracketed values with the values you generated above for the OIDC app using the Okta CLI.

You can run the bootstrapped project right now and see if it starts. It should start but won’t do much.

./gradlew bootRun

Create a SecurityConfiguration class to configure Spring Security. The class below configures web security to allow all requests, effectively bypassing security. This is just so you can test the resource server initially. You’ll enable security shortly.

src/main/java/com/example/demo/SecurityConfiguration.java

package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
            .anyRequest().permitAll();
        return http.build();
    }

}

Replace the DemoApplication.java file with the following.

src/main/java/com/example/demo/DemoApplication.java

package com.example.demo;

import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;

import java.util.Collections;
import java.util.Random;
import java.util.stream.Stream;

@SpringBootApplication
public class DemoApplication {

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

    // Bootstrap some test data into the in-memory database
    @Bean
    ApplicationRunner init(TodoRepository repository) {
        return args -> {
            Random rd = new Random();
            Stream.of("Buy milk", "Eat pizza", "Update tutorial", "Study Vue", "Go kayaking").forEach(name -> {
                Todo todo = new Todo();
                todo.setTitle(name);
                todo.setCompleted(rd.nextBoolean());
                repository.save(todo);
            });
            repository.findAll().forEach(System.out::println);
        };
    }

    // Fix the CORS errors
    @Bean
    public FilterRegistrationBean simpleCorsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        // *** URL below needs to match the Vue client URL and port ***
        config.setAllowedOrigins(Collections.singletonList("http://localhost:8080"));
        config.setAllowedMethods(Collections.singletonList("*"));
        config.setAllowedHeaders(Collections.singletonList("*"));
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean<>(new CorsFilter(source));
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }

    // Expose IDs of Todo items
    @Component
    class RestRepositoryConfigurator implements RepositoryRestConfigurer {
        public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
            config.exposeIdsFor(Todo.class);
        }
    }
    
}

This demo application does three things that are helpful for demonstration purposes. First, it loads some test todo items into the repository.

Second, it configures the REST repository to expose IDs for the todo items.

Third, it defines a filter to allow cross-origin requests from http://localhost:8080. This is necessary so that the Vue application, which is loaded from http://localhost:9000 via the local test server, can load data from the Spring Boot resource server at http://localhost:8080.

For more info on CORS (cross-origin resource sharing), take a look at the Mozilla docs.

Now, create the data model for the todo items.

src/main/java/com/example/demo/Todo.java

package com.example.demo;

import lombok.*;

import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Entity;

@Entity
@Data
@NoArgsConstructor
public class Todo {

    @Id 
    @GeneratedValue
    private Long id;

    @NonNull
    private String title;

    private Boolean completed = false;
    
}

Notice the use of the Lombok annotations (@Entity, @Data, and @NoArgsConstructor) to keep the code simple and clean.

The todo items have two fields: a title string and a completed boolean. The fields are annotated with Spring Data JPA annotations that allow the Java class to be mapped to a database table for persistence.

Create a repository to persist the data model.

src/main/java/com/example/demo/TodoRepository.java

package com.example.demo;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource
interface TodoRepository extends JpaRepository<Todo, Long> {}

This is a Spring Data JpaRepository that can persist the data model you just defined. Because it is annotated with @RepositoryRestResource (and because the data-rest dependency was included), this repository will be automatically exposed as a web resource.

Test your Vue and Spring Boot app

Run the app using the following command from the resource-server subdirectory.

./gradlew bootRun

Open a new Bash shell and use HTTPie to test the resource server.

http :9000/todos

You should see a response like the following:

HTTP/1.1 200 
...

{
  "_embedded": {
    "todos": [
      {
        "_links": {
          "self": {
            "href": "http://localhost:9000/todos/1"
          },
          "todo": {
            "href": "http://localhost:9000/todos/1"
          }
        },
        "completed": false,
        "id": 1,
        "title": "Buy milk"
      },
      {
        "_links": {
          "self": {
            "href": "http://localhost:9000/todos/2"
          },
          "todo": {
            "href": "http://localhost:9000/todos/2"
          }
        },
        "completed": true,
        "id": 2,
        "title": "Eat pizza"
      },
      ...
    ]
  },
  ...
}

Stop the resource server using CTRL + C.

Secure your Spring Boot API

Edit the SecurityConfiguration.java file and change the filter chain’s bean definition to enable a resource server.

src/main/java/com/example/demo/OAuth2ResourceServerSecurityConfiguration.java

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests()
        .anyRequest().authenticated()
        .and()
        .oauth2ResourceServer().jwt();
    return http.build();
}

This configuration requires JWT auth on all requests.

Restart the server. Use CTRL + C to stop it if it’s running.

./gradlew bootRun

Use HTTPie again to try and request the todo items.

http :9000/todos

You will get an error.

HTTP/1.1 401 
...

401 Unauthorized

The resource server is finished. The next step is to create the Vue client.

Create a Vue JavaScript client

Use the Vue CLI to create a new application from the project’s root directory and navigate into the newly created client directory. Install the Vue CLI if you don’t have it installed with npm i -g @vue/cli@5.

vue create client

Pick Default ([Vue 3] babel, eslint) when prompted. Wait for it to finish.

cd client

Add the Quasar framework.

vue add quasar

You can just accept the defaults. For me, they were the following.

  • Allow Quasar to replace App.vue, About.vue, Home.vue and (if available) router.js? Yes
  • Pick your favorite CSS preprocessor: Sass with indented syntax
  • Choose Quasar Icon Set: Material Icons (recommended)
  • Default Quasar language pack: en-US
  • Use RTL support? No
  • Select features: Enter to select none

Add additional dependencies for HTTP requests, logging, routing, and authentication.

npm i axios@1.2.3 vuejs3-logger@1.0.0 vue-router@4.1.6 @okta/okta-vue@5.5.0
  • axios: an HTTP client request library
  • vuejs3-logger: a logging library
  • vue-router: the standard for routing between pages in Vue
  • okta/okta-vue: the Okta helper for Vue

To learn more about how Okta integrates with Vue, look at the GitHub page for the okta/okta-vue project. More resources and example applications are listed in the Okta docs for Vue.

Replace main.js with the following. Look at the OktaAuth configuration object. Notice the client ID and issuer URI are pulled from a .env file.

src/main.js

import { createApp } from 'vue'
import App from './App.vue'
import { Quasar } from 'quasar'
import quasarUserOptions from './quasar-user-options'
import VueLogger from 'vuejs3-logger'
import router from './router'
import createApi from './Api'

import { OktaAuth } from '@okta/okta-auth-js'
import OktaVue from '@okta/okta-vue'

if (process.env.VUE_APP_ISSUER_URI == null || process.env.VUE_APP_CLIENT_ID == null || process.env.VUE_APP_SERVER_URI == null) {
  throw 'Please define VUE_APP_ISSUER_URI, VUE_APP_CLIENT_ID, and VUE_APP_SERVER_URI in .env file'
}

const oktaAuth = new OktaAuth({
  issuer: process.env.VUE_APP_ISSUER_URI,  // pulled from .env file
  clientId: process.env.VUE_APP_CLIENT_ID,  // pulled from .env file
  redirectUri: window.location.origin + '/callback',
  scopes: ['openid', 'profile', 'email']
})

const options = {
  isEnabled: true,
  logLevel: 'debug',
  stringifyArguments: false,
  showLogLevel: true,
  showMethodName: false,
  separator: '|',
  showConsoleColors: true
};

const app = createApp(App)
  .use(Quasar, quasarUserOptions)
  .use(VueLogger, options)
  .use(OktaVue, {oktaAuth})
  .use(router)

app.config.globalProperties.$api = createApi(app.config.globalProperties.$auth)

app.mount('#app')

Stated very briefly, the file above creates the main Vue app and configures it to use the dependencies you added: Quasar, VueLogger, OktaVue, and the router. It also creates the API class that handles the requests to the resource server and passes it the $auth object it needs to get the JWT.

Create a .env file in the client project root directory. The Client ID and Issuer URI are the values you used above in the Spring Boot application.properties file. The Server URI is the local URI for the Spring Boot API. You can leave this unless you made a change (this gets used in the Api.js file).

.env

VUE_APP_CLIENT_ID=<your-client-id>
VUE_APP_ISSUER_URI=<your-issuer-uri>
VUE_APP_SERVER_URI=http://localhost:9000

It’s important to note that putting values like this in a .env file in a client application does not make them secure. It helps by keeping them out of a repository. However, they are still public because they are necessarily visible in the JavaScript code sent to the browser. In this use case, it’s more of a configuration and organizational tool than a security tool.

If you want to keep the .env file out of the repository, you need to update the .gitignore file. There’s no particular need to do this for the Client ID as it will be publicly available anyway.

Replace App.vue with the following.

src/App.vue

<template>
  <q-layout view="hHh lpR fFf">

    <q-header elevated class="bg-primary text-white">
      <q-toolbar>
        <q-toolbar-title>
          <q-avatar>
            <q-icon name="kayaking" size="30px"></q-icon>
          </q-avatar>
          Todo App
        </q-toolbar-title>
        {{ this.claims && this.claims.email ? claims.email : '' }}
        <q-btn flat round dense icon="logout" v-if='authState && authState.isAuthenticated' @click="logout"/>
        <q-btn flat round dense icon="account_circle" v-else @click="login"/>
      </q-toolbar>
    </q-header>

    <q-page-container>
      <router-view></router-view>
    </q-page-container>

  </q-layout>
</template>

<script>
export default {
  name: 'LayoutDefault',
  data: function () {
    return {
      claims: null
    }
  },
  watch: {
    'authState.isAuthenticated'() {
      this.$log.debug(('watch triggered!'))
      this.updateClaims()
    }
  },
  created() {
    this.updateClaims()
  },
  methods: {
    async updateClaims() {
      if (this.authState && this.authState.isAuthenticated) {
        this.claims = await this.$auth.getUser()
      }
    },
    async login() {
      await this.$auth.signInWithRedirect({ originalUri: '/todos' })
    },
    async logout() {
      await this.$auth.signOut()
    }
  },
}
</script>

This top-level component defines the header bar and includes the router component. The header bar has a login or logout button and will show the authenticated user’s email address when logged in.

The app gets the authenticated user’s email address from the JWT claims. (A claim is a piece of information asserted about the subject by the authenticating authority.) This happens in the updateClaims() method, which is triggered when the component is created, and is also triggered by a watch method so that it is updated as the authenticated state changes.

Create a new file to encapsulate the resource server access logic.

src/Api.js

import axios from 'axios'

const instance = axios.create({
  baseURL: process.env.VUE_APP_SERVER_URI,
  timeout: 2000
});

const createApi = (auth) => {

  instance.interceptors.request.use(async function (config) {
    const accessToken = auth.getAccessToken()
    config.headers = {
      Authorization: `Bearer ${accessToken}`
    }
    return config;
  }, function (error) {
    return Promise.reject(error);
  });

  return {

    // (C)reate
    createNew(text, completed) {
      return instance.post('/todos', {title: text, completed: completed})
    },

    // (R)ead
    getAll() {
      return instance.get('/todos', {
        transformResponse: [function (data) {
          return data ? JSON.parse(data)._embedded.todos : data;
        }]
      })
    },

    // (U)pdate
    updateForId(id, text, completed) {
      return instance.put('todos/' + id, {title: text, completed: completed})
    },

    // (D)elete
    removeForId(id) {
      return instance.delete('todos/' + id)
    }
  }
}

export default createApi

All of the requests to the server go through this module. Take a look at how the access token is retrieved from the global auth object and injected into every request.

Create the router file.

src/router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import { navigationGuard } from '@okta/okta-vue'
import Todos from '@/components/Todos';
import Home from '@/components/Home';
import { LoginCallback } from '@okta/okta-vue'

const routes = [
  {
    path: '/',
    component: Home
  },
  {
    path: '/todos',
    component: Todos,
    meta: {
      requiresAuth: true
    }
  },
  { path: '/callback', component: LoginCallback },
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
})

router.beforeEach(navigationGuard)

export default router

The router has three paths. The home path and the todos path are straightforward. The Okta Vue SDK provides the last path, /callback, to handle the login redirect from the Okta servers after authentication.

Create the Home component.

src/components/Home.vue

<template>
  <div class="column justify-center items-center" id="row-container">
    <q-card class="my-card">
      <q-card-section style="text-align: center">
        <div v-if='authState && authState.isAuthenticated'>
          <h6 v-if="claims && claims.email">You are logged in as {{ claims.email }}</h6>
          <h6 v-else>You are logged in</h6>
          <q-btn flat color="primary" @click="todo">Go to Todo app</q-btn>
          <q-btn flat @click="logout">Log out</q-btn>
        </div>
        <div v-else>
          <h6>Please <a href="#" @click.prevent="login">log in</a> to access Todo app</h6>
        </div>
      </q-card-section>
    </q-card>
  </div>
</template>

<script>
export default {
  name: 'home-component',
  data: function () {
    return {
      claims: ''
    }
  },
  created() { 
    this.setup() 
  },
  methods: {
    async setup() {
      if (this.authState && this.authState.isAuthenticated) {
        this.claims = await this.$auth.getUser()
      }
    },
    todo() {
      this.$router.push('/todos')
    },
    async login() {
      await this.$auth.signInWithRedirect({ originalUri: '/todos' })
    },
    async logout() {
      await this.$auth.signOut()
    }
  }
}
</script>

Create the TodoItem component.

src/components/TodoItem.vue

<template>
  <q-item-section avatar class="check-icon" v-if="this.item.completed">
    <q-icon color="green" name="done" @click="handleClickSetCompleted(false)"/>
  </q-item-section>
  <q-item-section avatar class="check-icon" v-else>
    <q-icon color="gray" name="check_box_outline_blank" @click="handleClickSetCompleted(true)"/>
  </q-item-section>
  <q-item-section v-if="!editing">{{ this.item.title }}</q-item-section>
  <q-item-section v-else>
    <input
        class="list-item-input"
        type="text"
        name="textinput"
        ref="input"
        v-model="editingTitle"
        @change="handleDoneEditing"
        @blur="handleCancelEditing"
    />
  </q-item-section>
  <q-item-section avatar class="hide-icon" @click="handleClickEdit">
    <q-icon color="primary" name="edit"/>
  </q-item-section>
  <q-item-section avatar class="hide-icon close-icon" @click="handleClickDelete">
    <q-icon color="red" name="close"/>
  </q-item-section>
</template>
<script>

import { nextTick } from 'vue'

export default {
  name: 'TodoItem',
  props: {
    item: Object,
    deleteMe: Function,
    showError: Function,
    setCompleted: Function,
    setTitle: Function
  },
  data: function () {
    return {
      editing: false,
      editingTitle: this.item.title,
    }
  },
  methods: {
    handleClickEdit() {
      this.editing = true
      this.editingTitle = this.item.title
      nextTick(function () {
        this.$refs.input.focus()
      }.bind(this))
    },
    handleCancelEditing() {
      this.editing = false
    },
    handleDoneEditing() {
      this.editing = false
      this.$api.updateForId(this.item.id, this.editingTitle, this.item.completed).then((response) => {
        this.setTitle(this.item.id, this.editingTitle)
        this.$log.info('Item updated:', response.data);
      }).catch((error) => {
        this.showError('Failed to update todo title')
        this.$log.debug(error)
      });
    },
    handleClickSetCompleted(value) {
      this.$api.updateForId(this.item.id, this.item.title, value).then((response) => {
        this.setCompleted(this.item.id, value)
        this.$log.info('Item updated:', response.data);
      }).catch((error) => {
        this.showError('Failed to update todo completed status')
        this.$log.debug(error)
      });
    },
    handleClickDelete() {
      this.deleteMe(this.item.id)
    }
  }
}
</script>

<style scoped>
.todo-item .close-icon {
  min-width: 0px;
  padding-left: 5px !important;
}

.todo-item .hide-icon {
  opacity: 0.1;
}

.todo-item:hover .hide-icon {
  opacity: 0.8;
}

.check-icon {
  min-width: 0px;
  padding-right: 5px !important;
}

input.list-item-input {
  border: none;
}
</style>

This component encapsulates a single todo item. It has logic for editing the title, setting the completed status, and deleting items. If you look closely at the code, you’ll notice that it sends changes to the server and updates the local copy stored in the todos array in the parent component.

Create the Todos component.

src/components/Todos.vue

<template>
  <div class="column justify-center items-center" id="row-container">
    <q-card class="my-card">
      <q-card-section>
        <div class="text-h4">Todos</div>
        <q-list padding>
          <q-item
              v-for="item in filteredTodos" :key="item.id"
              clickable
              v-ripple
              rounded
              class="todo-item"
          >
            <TodoItem
                :item="item"
                :deleteMe="handleClickDelete"
                :showError="handleShowError"
                :setCompleted="handleSetCompleted"
                :setTitle="handleSetTitle"
                v-if="filter === 'all' || (filter === 'incomplete' && !item.completed) || (filter === 'complete' && item.completed)"
            ></TodoItem>
          </q-item>
        </q-list>
      </q-card-section>
      <q-card-section>
        <q-item>
          <q-item-section avatar class="add-item-icon">
            <q-icon color="green" name="add_circle_outline"/>
          </q-item-section>
          <q-item-section>
            <input
                type="text"
                ref="newTodoInput"
                v-model="newTodoTitle"
                @change="handleDoneEditingNewTodo"
                @blur="handleCancelEditingNewTodo"
            />
          </q-item-section>
        </q-item>
      </q-card-section>
      <q-card-section style="text-align: center">
        <q-btn color="amber" text-color="black" label="Remove Completed" style="margin-right: 10px" 
               @click="handleDeleteCompleted"></q-btn>
        <q-btn-group>
          <q-btn glossy :color="filter === 'all' ? 'primary' : 'white'" text-color="black" label="All" 
                 @click="handleSetFilter('all')"/>
          <q-btn glossy :color="filter === 'complete' ? 'primary' : 'white'" text-color="black" label="Completed" 
                 @click="handleSetFilter('complete')"/>
          <q-btn glossy :color="filter === 'incomplete' ? 'primary' : 'white'" text-color="black" label="Incomplete" 
                 @click="handleSetFilter('incomplete')"/>
          <q-tooltip>
            Filter the todos
          </q-tooltip>
        </q-btn-group>
      </q-card-section>
    </q-card>
    <div v-if="error" class="error">
      <q-banner inline-actions class="text-white bg-red" @click="handleErrorClick">
        ERROR: {{ this.error }}
      </q-banner>
    </div>
  </div>
</template>

<script>

import TodoItem from '@/components/TodoItem';
import { ref } from 'vue'

export default {
  name: 'LayoutDefault',
  components: {
    TodoItem
  },

  data: function() {
    return {
      todos: [],
      newTodoTitle: '',
      visibility: 'all',
      loading: true,
      error: '',
      filter: 'all'
    }
  },

  setup() {
    return {
      alert: ref(false),
    }
  },
  mounted() {
    this.$api.getAll()
        .then(response => {
          this.$log.debug('Data loaded: ', response.data)
          this.todos = response.data
        })
        .catch(error => {
          this.$log.debug(error)
          this.error = 'Failed to load todos'
        })
        .finally(() => this.loading = false)
  },

  computed: {
    filteredTodos() {
      if (this.filter === 'all') return this.todos
      else if (this.filter === 'complete') return this.todos.filter(todo => todo.completed)
      else if (this.filter === 'incomplete') return this.todos.filter(todo => !todo.completed)
      else return []
    }
  },

  methods: {
    handleSetFilter(value) {
      this.filter = value
    },

    handleClickDelete(id) {
      const todoToRemove = this.todos.find(todo => todo.id === id)
      this.$api.removeForId(id).then(() => {
        this.$log.debug('Item removed:', todoToRemove);
        this.todos.splice(this.todos.indexOf(todoToRemove), 1)
      }).catch((error) => {
        this.$log.debug(error);
        this.error = 'Failed to remove todo'
      });
    },

    handleDeleteCompleted() {
      const completed = this.todos.filter(todo => todo.completed)
      Promise.all(completed.map(todoToRemove => {
        return this.$api.removeForId(todoToRemove.id).then(() => {
          this.$log.debug('Item removed:', todoToRemove);
          this.todos.splice(this.todos.indexOf(todoToRemove), 1)
        }).catch((error) => {
          this.$log.debug(error);
          this.error = 'Failed to remove todo'
          return error
        })
      }))
    },

    handleDoneEditingNewTodo() {
      const value = this.newTodoTitle && this.newTodoTitle.trim()
      if (!value) {
        return
      }
      this.$api.createNew(value, false).then((response) => {
        this.$log.debug('New item created:', response)
        this.newTodoTitle = ''
        this.todos.push({
          id: response.data.id,
          title: value,
          completed: false
        })
        this.$refs.newTodoInput.blur()
      }).catch((error) => {
        this.$log.debug(error);
        this.error = 'Failed to add todo'
      });
    },
    handleCancelEditingNewTodo() {
      this.newTodoTitle = ''
    },

    handleSetCompleted(id, value) {
      let todo = this.todos.find(todo => id === todo.id)
      todo.completed = value
    },

    handleSetTitle(id, value) {
      let todo = this.todos.find(todo => id === todo.id)
      todo.title = value
    },

    handleShowError(message) {
      this.error = message
    },

    handleErrorClick() {
      this.error = null;
    },
  },
}
</script>

<style>
#row-container {
  margin-top: 100px;
}

.my-card {
  min-width: 600px;
}

.error {
  color: red;
  text-align: center;
  min-width: 600px;
  margin-top: 10px;
}
</style>

This component encapsulates the card that holds all of the todos and the todo-associated interface elements. It also handles the rest of the functions related to updating todos on the server and in the local cache.

You’re welcome to delete the HelloWorld.vue component if you want. Or you can leave it. It’s not needed.

Confirm your Spring Boot and Vue todo app works

Make sure the Spring Boot API is still running. In a separate Bash shell, from the resource server directory, run the following command (if it is not already still running).

./gradlew bootRun

Start the Vue app using the embedded development server. From the client directory:

npm run serve

Open a browser and navigate to http://localhost:8080. You’ll see the “please log in” page.

Please log in

Log into the app using Okta’s sign-in interface.

Okta SSO login

That will redirect you to the Todo app’s main screen.

Todo app main screen

You should be able to delete items, add new items, rename, and filter items. All data is stored on the Spring Boot resource server and is presented by the Vue + Quasar frontend.

Use Auth0 to secure the API

You can also use Auth0 to secure the application! Let’s start with the API (in the resource-server directory of the GitHub repo or your main project).

The first step is to open the build.gradle file for the Spring Boot project and update the dependencies. You have to remove the Okta Spring Boot Starter (as it does not work with Auth0 yet) and add in some Spring Security dependencies that were being included by the Okta starter.

Update the implementation dependencies in build.gradle.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-data-rest'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.security:spring-security-oauth2-resource-server'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.security:spring-security-config'
    implementation 'org.springframework.security:spring-security-oauth2-jose'

    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Create an AudienceValidator class. This will validate JWTs very simply by checking to make sure the audience matches what is loaded from the application properties and passed into the constructor. src/main/java/com/example/demo/AudienceValidator.java

package com.example.demo;

import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;

class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    private final String audience;

    AudienceValidator(String audience) {
        this.audience = audience;
    }

    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);

        if (jwt.getAudience().contains(audience)) {
            return OAuth2TokenValidatorResult.success();
        }
        return OAuth2TokenValidatorResult.failure(error);
    }
}

You need to add a JWT validator bean to the security configuration class. This uses the AudienceValidator class you added above to validate JWTs. Update the SecurityConfiguration class to the following.

src/main/java/com/example/demo/SecurityConfiguration.java

package com.example.demo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
            .anyRequest().authenticated()
            .and()
            .oauth2ResourceServer().jwt();
        return http.build();
    }

    @Value("${auth0.audience}")
    private String audience;

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuer;

    @Bean
    JwtDecoder jwtDecoder() {
        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
            JwtDecoders.fromOidcIssuerLocation(issuer);

        OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

        jwtDecoder.setJwtValidator(withAudience);

        return jwtDecoder;
    }
}

Install the Auth0 CLI and run auth0 login in a terminal.

Waiting for the login to complete in the browser... done

 ▸    Successfully logged in.
 ▸    Tenant: dev-0xb84jzp.us.auth0.com

Take note of the domain listed as the tenant. This is your Auth0 domain. If you need to find it again later, you can use auth0 tenants list.

Update src/main/resources/application.properties. Fill in your actual Auth0 domain.

server.port=9000
auth0.audience=http://my-api
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://<your-auth0-domain>/

Start the API.

./gradlew bootRun

Make sure it starts successfully.

2022-10-06 10:09:59.535  INFO 89160 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 9000 (http) with context path ''
2022-10-06 10:09:59.541  INFO 89160 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 3.014 seconds (JVM running for 3.252)
Todo(id=1, title=Buy milk, completed=false)
Todo(id=2, title=Eat pizza, completed=false)
Todo(id=3, title=Update tutorial, completed=true)
Todo(id=4, title=Study Vue, completed=false)
Todo(id=5, title=Go kayaking, completed=true)
<==========---> 80% EXECUTING [2m 58s]
> :bootRun

Open a second terminal window in the same directory. Create a test Auth0 API. The Auth0 API is what exposes identity functionality for all authentication and authorization protocols, such as OpenID Connect and OAuth.

auth0 apis create -n myapi --identifier http://my-api

Just press enter three times to accept the default values for scopes, token lifetime, and allow offline access. The scopes here refer to custom scopes, not the standard scopes (email, profile, and openid) that you will need for OIDC and OAuth.

 Scopes: 
 Token Lifetime: 86400
 Allow Offline Access: No

=== dev-0xb84jzp.us.auth0.com API created

  ID                    6323478u98u98919206c2f73e6d  
  NAME                  myapi                     
  IDENTIFIER            http://my-api             
  SCOPES                                          
  TOKEN LIFETIME        86400                     
  ALLOW OFFLINE ACCESS  ✗      

Use Auth0 CLI to create a token. Don’t forget to set the audience!

auth0 test token -a http://my-api

If you don’t use the -a flag to set the audience to your Auth0 API, the test token you create will be an opaque token that cannot be verified and will not work. If you decide to use a different Auth0 API for some reason, you need to make sure the audience identifiers match in the application.properties file and the command to create a test token.

Save the token in a shell variable.

TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im5yMWZw...

You can verify that the endpoint is protected.

http :9000/todos

And test the protected endpoint using the token.

http :9000/todos "Authorization: Bearer $TOKEN"

Update the Vue client to use Auth0

Auth0 has helpful docs for integrating with Vue. The first step is to create an OpenID Connect (OIDC) application on the Auth0 servers using their CLI. Open a terminal and navigate to the client project directory.

auth0 apps create
  • Name: vue-spring-boot
  • Type: Single Page Web Application
  • All the URLs: http://localhost:8080
 Name: vue-spring-boot
 Description: 
 Type: Single Page Web Application
 Callback URLs: http://localhost:8080
 Allowed Logout URLs: http://localhost:8080
 Allowed Origin URLs: http://localhost:8080
 Allowed Web Origin URLs: http://localhost:8080

=== dev-0rb77iup.us.auth0.com application created

Update the .env file. Fill in the OIDC Client ID and Auth0 domain.

VUE_APP_CLIENT_ID=<your-client-id>
VUE_APP_AUTH0_DOMAIN=<your-auth0-domain>
VUE_APP_AUTH0_AUDIENCE=http://my-api
VUE_APP_SERVER_URI=http://localhost:9000

Notice that the audience is the same as the audience used to create the test token, which is the Auth0 API.

Install the Auth0 Vue SDK. Make sure you’re in the client directory.

npm install @auth0/auth0-vue@2

If you want, you can remove the Okta Vue SDK.

npm remove @okta/okta-vue

Update src/main.js to the following. This configures and installs the Auth0 plugin for Vue.

import { createApp } from 'vue'
import App from './App.vue'
import { Quasar } from 'quasar'
import quasarUserOptions from './quasar-user-options'
import VueLogger from 'vuejs3-logger'
import router from './router'
import createApi from './Api'

import { createAuth0 } from '@auth0/auth0-vue';

const options = {
  isEnabled: true,
  logLevel: 'debug',
  stringifyArguments: false,
  showLogLevel: true,
  showMethodName: false,
  separator: '|',
  showConsoleColors: true
};

const app = createApp(App)
  .use(Quasar, quasarUserOptions)
  .use(VueLogger, options)
  .use(router)
  .use(createAuth0({
      domain: process.env.VUE_APP_AUTH0_DOMAIN,
      clientId: process.env.VUE_APP_CLIENT_ID,
      authorizationParams: {
        redirect_uri: window.location.origin,
        audience: process.env.VUE_APP_AUTH0_AUDIENCE
      }
    })
  );

// pass auth0 to the api (to get a JWT), which is set as a global property
app.config.globalProperties.$api = createApi(app.config.globalProperties.$auth0)

app.mount('#app')

Update one line in src/Api.js. You need to change the following line.

const accessToken = auth.getAccessToken()

To this.

const accessToken = await auth.getAccessTokenSilently();

As is seen below.

import axios from 'axios'

...

const createApi = (auth) => {

  instance.interceptors.request.use(async function (config) {
    const accessToken = await auth.getAccessTokenSilently(); // UPDATE ME
    config.headers = {
      Authorization: `Bearer ${accessToken}`
    }
    return config;
  }, function (error) {
    return Promise.reject(error);
  });

  ...
 
}

export default createApi

Update src/App.vue.

<template>
  <q-layout view="hHh lpR fFf">

    <q-header elevated class="bg-primary text-white">
      <q-toolbar>
        <q-toolbar-title>
          <q-avatar>
            <q-icon name="kayaking" size="30px"></q-icon>
          </q-avatar>
          Todo App
        </q-toolbar-title>
        {{ isAuthenticated ? user.email : "" }}
        <q-btn flat round dense icon="logout" v-if='isAuthenticated' @click="logout"/>
        <q-btn flat round dense icon="account_circle" v-else @click="login"/>
      </q-toolbar>
    </q-header>

    <q-page-container>
      <router-view></router-view>
    </q-page-container>

  </q-layout>
</template>

<script>

import { useAuth0 } from '@auth0/auth0-vue';

export default {
  setup() {

    const { loginWithRedirect, user, isAuthenticated, logout } = useAuth0();

    return {
      login: () => {
        loginWithRedirect();
      },
      logout: () => {
        logout({ logoutParams: { returnTo: window.location.origin } });
      },
      user,
      isAuthenticated
    };
  }
}
</script>

Update src/components/Home.vue.

<template>
  <div class="column justify-center items-center" id="row-container">
    <q-card class="my-card">
      <q-card-section style="text-align: center">
        <div v-if='isAuthenticated'>
          <h6>You are logged in as {{user.email}}</h6>
          <q-btn flat color="primary" @click="todo">Go to Todo app</q-btn>
          <q-btn flat @click="logout">Log out</q-btn>
        </div>
        <div v-else>
          <h6>Please <a href="#" @click.prevent="login">log in</a> to access Todo app</h6>
        </div>
      </q-card-section>
    </q-card>
  </div>
</template>

<script>

import { useAuth0 } from '@auth0/auth0-vue';
import { useRouter } from 'vue-router'

export default {
  name: 'HomeComponent',
  setup() {

    const { loginWithRedirect, user, isAuthenticated, logout } = useAuth0();
    const router = useRouter()

    return {
      login: () => {
        loginWithRedirect();
      },
      logout: () => {
        logout({ returnTo: window.location.origin });
      },
      todo() {
        router.push('/todos')
      },
      user,
      isAuthenticated
    };
  }
}
</script>

Finally, update src/router/index.js.

import { createRouter, createWebHistory } from 'vue-router'
import Todos from '@/components/Todos';
import Home from '@/components/Home';

const routes = [
  {
    path: '/',
    component: Home
  },
  {
    path: '/todos',
    component: Todos,
    meta: {
      requiresAuth: true
    }
  },
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
})

export default router

The usage of the Auth0 SDK is pretty similar to the Okta Vue SDK. If you have any questions, take a look at the auth0-vue GitHub repository.

Make sure your Spring Boot API is still running. Run the client.

npm run serve

This time when you log in you will be directed to Auth0.

Auth0 Login

After that, you will be redirected back to the todo app.

Original article source at https://developer.okta.com

#springboot #vue #java 

Build a Simple CRUD App with Spring Boot and Vue.js
Alfie Mellor

Alfie Mellor

1675064047

Full Stack App with React and Spring Boot

Learn how to build a full-stack todo app with a Spring Boot Java backend and a React Typescript frontend connected with the new Hilla framework. 

Full-stack React + Spring Boot app tutorial [Java + TypeScript + Hilla]
 

Create the app with:
npx @hilla/cli init --react todo

0:00 - Intro
0:18 - Creating a Hilla Spring Boot + React project
1:08 - Adding database dependencies
1:50 - Starting the dev server (Spring Boot and Vite)
2:37 - Creating a Todo View
4:32 - Creating the backend data model
6:40 - Creating an Endpoint server API
9:20 - Fetching todos from the server
12:38 - Adding new todos: input and server call
16:57 - Listing todos
18:03 - Updating todos
20:55 - Outro

#react #springboot #java #typescript 

Full Stack App with React and Spring Boot
Brooke  Giles

Brooke Giles

1674786403

Full Stack Development with Java Spring Boot, React and MongoDB

Learn how to create an application using MongoDb for the database, Java and Spring Boot for the backend, and React for the frontend.

Full Stack Development with Java Spring Boot, React, and MongoDB – Full Course

In this full stack development course, you will learn how to create an application using MongoDb for the database, Java and Spring Boot for the backend, and React for the frontend.

You will learn to develop a movie review application that will feature a separation of concerns between the client code and the server code. By implementing this loosely coupled architecture, these two parts (implemented using different technologies) can evolve in parallel and independently from one another.

⭐️ Contents ⭐️
⌨️ (0:00:00) Course Introduction
⌨️ (0:01:48) JDK and IntelliJ IDEA Installation
⌨️ (0:06:13) MongoDB Atlas
⌨️ (0:20:35) Project Initialization
⌨️ (0:27:03) Project Structure
⌨️ (0:29:13) Running The Project
⌨️ (0:32:41) Writing The First Endpoint
⌨️ (0:35:33) Database Configuration
⌨️ (0:38:49) Installing Additional Dependencies
⌨️ (0:44:48) Setting Up Environment Variables
⌨️ (0:47:20) Movies and Reviews Endpoints
⌨️ (1:31:46) Testing
⌨️ (1:35:42) Backend Conclusion

⌨️ (1:36:45) Frontend Introduction
⌨️ (1:38:32) Create the React Project
⌨️ (1:43:49) Applying Bootstrap to our React application
⌨️ (1:52:47) Implementing the Use State and Use Effect Hooks
⌨️ (1:58:04) Create Home and Hero Component
⌨️ (2:06:56) Style the Carousel
⌨️ (2:15:21) Create Header Component (Navigation)
⌨️ (2:21:55) Create Trailer Component with react-player
⌨️ (2:28:46) Create Movie Reviews Functionality
⌨️ (2:34:43) Add and Get Reviews with HTTP Requests
⌨️ (2:40:33) Course Wrap Up

💻 Backend Code: https://github.com/fhsinchy/movieist 
💻 Frontend Code: https://github.com/GavinLonDigital/movie-gold-v1 

🔗 Spring Initializr - https://start.spring.io/ 
🔗 JDK Download Page: https://www.oracle.com/java/technologies/downloads/ 
🔗 IntelliJ IDEA Download Page: https://www.jetbrains.com/idea/download/ 
🔗 Postman Download Page: https://www.postman.com/downloads/

#java #springboot #mongodb #fullstack #react 

Full Stack Development with Java Spring Boot, React and MongoDB

Build Full Stack Application using Spring Boot with Angular

This  tutorial will teach you how to do the full stack development application using Spring boot with Angular that are CREATE, RETIEVE, UPDATE and DELETE and SEARCH using mysql Database. The INSERT, SELECT, UPDATE and DELETE statements can be used in any database system, because this is support by all relational database systems.

Source code : https://www.tutussfunny.com/spring-boot-with-angular-full-stack-project/ 

Subscribe: https://www.youtube.com/@TutusFunny/featured 

#angular #springboot  

Build Full Stack Application using Spring Boot with Angular
Jade Bird

Jade Bird

1672801732

JWT Authentication in Spring Boot 3 App with Spring Security 6 & Postgres

In this tutorial, you'll learn how to implement JWT authentication and authorization in a Spring Boot 3.0 application using Spring Security 6 and a Postgres database to store user credentials.

You'll see how easy it is to secure your application and protect your endpoints using JSON Web Tokens. We'll start by setting up a Postgres database and creating a user table to store our credentials.

Then, we'll configure Spring Security to use JWT and define some security rules for our application. Finally, we'll test our setup by building a simple API and using Postman to send authenticated requests.

Whether you're a beginner or an experienced developer, this tutorial will give you the tools you need to secure your Spring Boot application with JWT authentication and authorization.

Code - https://github.com/ali-bouali/spring-boot-3-jwt-security 

#springboot3 #springsecurity #jwt #springboot 

 

JWT Authentication in Spring Boot 3 App with Spring Security 6 & Postgres
Zara  Bryant

Zara Bryant

1672380234

How to Build a CRUD App with React and Spring Boot

Learn how to build a Simple CRUD App with React and Spring Boot. And secure it all with Auth0 by Okta.

React is one of the most popular JavaScript frameworks, and Spring Boot is wildly popular in the Java ecosystem. This screencast shows you how to use them in the same app and secure it all with Auth0 by Okta.


Table of Contents:

00:00 - Hello, developers
01:20 - Install prerequisites
01:48 - Create an API app with Spring Boot
02:49 - Add a JPA domain model
08:19 - Create a React UI with Create React App
09:59 - Call your Spring Boot API and display the results
11:10 - Build a React GroupList component
13:20 - Add a React GroupEdit component
15:49 - Add Authentication with Auth0
21:35 - Add Authentication with Okta
24:37 - Configure Spring Security for React and user identity
32:06 - Modify React to handle CSRF and be identity-aware
36:10 - Configure Maven to build and package React with Spring Boot
40:51 - Thanks for watching!

Blog post: https://morioh.com/p/1fc70de845b5 
GitHub repo: https://github.com/oktadev/okta-spring-boot-react-crud-example 
Demo script: https://github.com/oktadev/okta-spring-boot-react-crud-example/blob/main/demo.adoc 
Questions: https://devforum.okta.com/t/use-react-and-spring-boot-to-build-a-simple-crud-app/21006 

#react #springboot #java

How to Build a CRUD App with React and Spring Boot
Khaitan

Khaitan

1672379638

How to Create a CRUD App with Spring Boot and React

Learn how to create a basic CRUD app with Spring Boot and React. In this tutorial, I’ll use the OAuth 2.0 Authorization Code flow and package the React app in the Spring Boot app for production. At the same time, I’ll show you how to keep React’s productive workflow for developing locally.

React is one of the most popular JavaScript frameworks, and Spring Boot is wildly popular in the Java ecosystem. This article shows you how to use them in the same app and secure it all with Okta.

React was designed to make it painless to create interactive UIs. Its state management is efficient and only updates components when your data changes. Component logic is written in JavaScript, meaning you can keep state out of the DOM and create encapsulated components.

Developers like CRUD (create, read, update, and delete) apps because they show a lot of the base functionality you need when creating an app. Once you have the basics of CRUD completed in an app, most of the client-server plumbing is finished, and you can move on to implementing the necessary business logic.

Today, I’ll show you how to create a basic CRUD app with Spring Boot and React. In this tutorial, I’ll use the OAuth 2.0 Authorization Code flow and package the React app in the Spring Boot app for production. At the same time, I’ll show you how to keep React’s productive workflow for developing locally.

This tutorial is also available as a screencast.

 

Prerequisites:

You will need Java 17 and Node 16 installed to complete this tutorial.

 

  • Create an API app with Spring Boot
    • Add a JPA domain model
  • Create a React UI with Create React App
  • Call your Spring Boot API and display the results
  • Build a React GroupList component
  • Add a React GroupEdit component
  • Add authentication with Okta
    • Spring Security + OIDC
    • Create an OIDC app in Okta
    • Use Auth0 for OIDC
  • Configure Spring Security for React and user identity
  • Modify React to handle CSRF and be identity-aware
  • Configure Maven to build and package React with Spring Boot
  • Learn more about Spring Boot and React

Create an API app with Spring Boot

I’m a frequent speaker at conferences and user groups around the world. My favorite user groups to speak at are Java User Groups (JUGs). I’ve been a Java developer for almost 20 years and love the Java community. One of my good friends, James Ward, said doing a JUG Tour was one of his favorite developer advocate activities back in the day. I recently took his advice and traded overseas conferences for JUG meetups in the US.

Why am I telling you this? Because I thought it’d be fun to create a “JUG Tours” app today that allows you to create/edit/delete JUGs and view upcoming events.

To begin, navigate to start.spring.io and make the following selections:

  • Project: Maven Project
  • Group: com.okta.developer
  • Artifact: jugtours
  • Dependencies: JPA, H2, Web, Lombok

Spring Initializr

Click Generate Project, expand jugtours.zip after downloading, and open the project in your favorite IDE.

TIP: If you’re using IntelliJ IDEA or Spring Tool Suite, you can also use Spring Initializr when creating a new project.

Add a JPA domain model

The first thing you’ll need to do is to create a domain model that’ll hold your data. At a high level, there’s a Group that represents the JUG, an Event that has a many-to-one relationship with Group, and a User that has a one-to-many relationship with Group.

Create a src/main/java/com/okta/developer/jugtours/model directory and a Group.java class in it.

package com.okta.developer.jugtours.model;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

import jakarta.persistence.*;
import java.util.Set;

@Data
@NoArgsConstructor
@RequiredArgsConstructor
@Entity
@Table(name = "user_group")
public class Group {

    @Id
    @GeneratedValue
    private Long id;
    @NonNull
    private String name;
    private String address;
    private String city;
    private String stateOrProvince;
    private String country;
    private String postalCode;
    @ManyToOne(cascade=CascadeType.PERSIST)
    private User user;

    @OneToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
    private Set<Event> events;
}

Create an Event.java class in the same package.

package com.okta.developer.jugtours.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import java.time.Instant;
import java.util.Set;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Event {

    @Id
    @GeneratedValue
    private Long id;
    private Instant date;
    private String title;
    private String description;
    @ManyToMany
    private Set<User> attendees;
}

And a User.java class.

package com.okta.developer.jugtours.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {

    @Id
    private String id;
    private String name;
    private String email;
}

Create a GroupRepository.java to manage the group entity.

package com.okta.developer.jugtours.model;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface GroupRepository extends JpaRepository<Group, Long> {
    Group findByName(String name);
}

To load some default data, create an Initializer.java class in the com.okta.developer.jugtours package.

package com.okta.developer.jugtours;

import com.okta.developer.jugtours.model.Event;
import com.okta.developer.jugtours.model.Group;
import com.okta.developer.jugtours.model.GroupRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.Collections;
import java.util.stream.Stream;

@Component
class Initializer implements CommandLineRunner {

    private final GroupRepository repository;

    public Initializer(GroupRepository repository) {
        this.repository = repository;
    }

    @Override
    public void run(String... strings) {
        Stream.of("Seattle JUG", "Denver JUG", "Dublin JUG",
                "London JUG").forEach(name ->
                repository.save(new Group(name))
        );

        Group djug = repository.findByName("Seattle JUG");
        Event e = Event.builder().title("Micro Frontends for Java Developers")
                .description("JHipster now has microfrontend support!")
                .date(Instant.parse("2022-09-13T17:00:00.000Z"))
                .build();
        djug.setEvents(Collections.singleton(e));
        repository.save(djug);

        repository.findAll().forEach(System.out::println);
    }
}

TIP: If your IDE has issues with Event.builder(), you need to turn on annotation processing and/or install the Lombok plugin. I had to uninstall/reinstall the Lombok plugin in IntelliJ IDEA to get things to work.

If you start your app (using ./mvnw spring-boot:run) it should result in something like:

Group(id=1, name=Seattle JUG, address=null, city=null, stateOrProvince=null, country=null, postalCode=null, user=null, 
  events=[Event(id=5, date=2022-09-13T17:00:00Z, title=Micro Frontends for Java Developers, description=JHipster now has microfrontend support!, attendees=[])])
Group(id=2, name=Denver JUG, address=null, city=null, stateOrProvince=null, country=null, postalCode=null, user=null, events=[])
Group(id=3, name=Dublin JUG, address=null, city=null, stateOrProvince=null, country=null, postalCode=null, user=null, events=[])
Group(id=4, name=London JUG, address=null, city=null, stateOrProvince=null, country=null, postalCode=null, user=null, events=[])

Add a GroupController.java class (in src/main/java/.../jugtours/web/GroupController.java) that allows you to CRUD groups.

package com.okta.developer.jugtours.web;

import com.okta.developer.jugtours.model.Group;
import com.okta.developer.jugtours.model.GroupRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Optional;

@RestController
@RequestMapping("/api")
class GroupController {

    private final Logger log = LoggerFactory.getLogger(GroupController.class);
    private GroupRepository groupRepository;

    public GroupController(GroupRepository groupRepository) {
        this.groupRepository = groupRepository;
    }

    @GetMapping("/groups")
    Collection<Group> groups() {
        return groupRepository.findAll();
    }

    @GetMapping("/group/{id}")
    ResponseEntity<?> getGroup(@PathVariable Long id) {
        Optional<Group> group = groupRepository.findById(id);
        return group.map(response -> ResponseEntity.ok().body(response))
                .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }

    @PostMapping("/group")
    ResponseEntity<Group> createGroup(@Valid @RequestBody Group group) throws URISyntaxException {
        log.info("Request to create group: {}", group);
        Group result = groupRepository.save(group);
        return ResponseEntity.created(new URI("/api/group/" + result.getId()))
                .body(result);
    }

    @PutMapping("/group/{id}")
    ResponseEntity<Group> updateGroup(@Valid @RequestBody Group group) {
        log.info("Request to update group: {}", group);
        Group result = groupRepository.save(group);
        return ResponseEntity.ok().body(result);
    }

    @DeleteMapping("/group/{id}")
    public ResponseEntity<?> deleteGroup(@PathVariable Long id) {
        log.info("Request to delete group: {}", id);
        groupRepository.deleteById(id);
        return ResponseEntity.ok().build();
    }
}

Add the following dependency to your pom.xml to fix compilation errors:

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

If you stop and start your server app and hit http://localhost:8080/api/groups with your browser, or a command line client, you should see the list of groups.

You can create, read, update, and delete groups with the following HTTPie commands.

http POST :8080/api/group name='Utah JUG' city='Salt Lake City' country=USA
http :8080/api/group/6
http PUT :8080/api/group/6 id=6 name='Utah JUG' address='On the slopes'
http DELETE :8080/api/group/6

Create a React UI with Create React App

Create React App is a command line utility that generates React projects for you. It’s a convenient tool because it also offers commands to build and optimize your project for production. It uses webpack under the covers to build everything.

Create a new project in the jugtours directory with npx.

npx create-react-app@5 app

After the app creation process completes, navigate into the app directory and install Bootstrap, cookie support for React, React Router, and Reactstrap.

cd app
npm i bootstrap@5 react-cookie@4 react-router-dom@6 reactstrap@9

You’ll use Bootstrap’s CSS and Reactstrap’s components to make the UI look better, especially on mobile phones. If you’d like to learn more about Reactstrap, see reactstrap.github.io. It has extensive documentation on Reactstrap’s various components and their use.

Add Bootstrap’s CSS file as an import in app/src/index.js.

import 'bootstrap/dist/css/bootstrap.min.css';

Call your Spring Boot API and display the results

Modify app/src/App.js to use the following code that calls /api/groups and displays the list in the UI.

import React, { useEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';

const App = () => {

  const [groups, setGroups] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);

    fetch('api/groups')
      .then(response => response.json())
      .then(data => {
        setGroups(data);
        setLoading(false);
      })
  }, []);

  if (loading) {
    return <p>Loading...</p>;
  }

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <div className="App-intro">
          <h2>JUG List</h2>
          {groups.map(group =>
            <div key={group.id}>
              {group.name}
            </div>
          )}
        </div>
      </header>
    </div>
  );
}

export default App;

TIP: I learned a lot about React Hooks from Build a CRUD App in React with Hooks by Tania Rascia.

To proxy from /api to http://localhost:8080/api, add a proxy setting to app/package.json.

"scripts": {...},
"proxy": "http://localhost:8080",

To learn more about proxying API requests, see Create React App’s documentation.

Make sure Spring Boot is running, then run npm start in your app directory. You should see the list of default groups.

JUG List

Build a React GroupList component

React is all about components, and you don’t want to render everything in your main App, so create app/src/GroupList.js and populate it with the following JavaScript.

import React, { useEffect, useState } from 'react';
import { Button, ButtonGroup, Container, Table } from 'reactstrap';
import AppNavbar from './AppNavbar';
import { Link } from 'react-router-dom';

const GroupList = () => {

  const [groups, setGroups] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);

    fetch('api/groups')
      .then(response => response.json())
      .then(data => {
        setGroups(data);
        setLoading(false);
      })
  }, []);

  const remove = async (id) => {
    await fetch(`/api/group/${id}`, {
      method: 'DELETE',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      }
    }).then(() => {
      let updatedGroups = [...groups].filter(i => i.id !== id);
      setGroups(updatedGroups);
    });
  }

  if (loading) {
    return <p>Loading...</p>;
  }

  const groupList = groups.map(group => {
    const address = `${group.address || ''} ${group.city || ''} ${group.stateOrProvince || ''}`;
    return <tr key={group.id}>
      <td style={{whiteSpace: 'nowrap'}}>{group.name}</td>
      <td>{address}</td>
      <td>{group.events.map(event => {
        return <div key={event.id}>{new Intl.DateTimeFormat('en-US', {
          year: 'numeric',
          month: 'long',
          day: '2-digit'
        }).format(new Date(event.date))}: {event.title}</div>
      })}</td>
      <td>
        <ButtonGroup>
          <Button size="sm" color="primary" tag={Link} to={"/groups/" + group.id}>Edit</Button>
          <Button size="sm" color="danger" onClick={() => remove(group.id)}>Delete</Button>
        </ButtonGroup>
      </td>
    </tr>
  });

  return (
    <div>
      <AppNavbar/>
      <Container fluid>
        <div className="float-end">
          <Button color="success" tag={Link} to="/groups/new">Add Group</Button>
        </div>
        <h3>My JUG Tour</h3>
        <Table className="mt-4">
          <thead>
          <tr>
            <th width="20%">Name</th>
            <th width="20%">Location</th>
            <th>Events</th>
            <th width="10%">Actions</th>
          </tr>
          </thead>
          <tbody>
          {groupList}
          </tbody>
        </Table>
      </Container>
    </div>
  );
};

export default GroupList;

Create AppNavbar.js in the same directory to establish a common UI feature between components.

import React, { useState } from 'react';
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
import { Link } from 'react-router-dom';

const AppNavbar = () => {

  const [isOpen, setIsOpen] = useState(false);

  return (
    <Navbar color="dark" dark expand="md">
      <NavbarBrand tag={Link} to="/">Home</NavbarBrand>
      <NavbarToggler onClick={() => { setIsOpen(!isOpen) }}/>
      <Collapse isOpen={isOpen} navbar>
        <Nav className="justify-content-end" style={{width: "100%"}} navbar>
          <NavItem>
            <NavLink href="https://twitter.com/oktadev">@oktadev</NavLink>
          </NavItem>
          <NavItem>
            <NavLink href="https://github.com/oktadev/okta-spring-boot-react-crud-example">GitHub</NavLink>
          </NavItem>
        </Nav>
      </Collapse>
    </Navbar>
  );
};

export default AppNavbar;

Create app/src/Home.js to serve as the landing page for your app.

import React from 'react';
import './App.css';
import AppNavbar from './AppNavbar';
import { Link } from 'react-router-dom';
import { Button, Container } from 'reactstrap';

const Home = () => {
  return (
    <div>
      <AppNavbar/>
      <Container fluid>
        <Button color="link"><Link to="/groups">Manage JUG Tour</Link></Button>
      </Container>
    </div>
  );
}

export default Home;

Also, change app/src/App.js to use React Router to navigate between components.

import React from 'react';
import './App.css';
import Home from './Home';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import GroupList from './GroupList';
import GroupEdit from './GroupEdit';

const App = () => {
  return (
    <Router>
      <Routes>
        <Route exact path="/" element={<Home/>}/>
        <Route path='/groups' exact={true} element={<GroupList/>}/>
      </Routes>
    </Router>
  )
}

export default App;

To make your UI a bit more spacious, add a top margin to Bootstrap’s container classes in app/src/App.css.

nav + .container, nav + .container-fluid {
  margin-top: 20px;
} 

Your React app should update itself as you make changes, and you should see a screen like the following at http://localhost:3000.

Home screen with Manage JUG Tour link

Click on Manage JUG Tour and you should see a list of the default groups.

Group List screen

It’s great that you can see your Spring Boot API’s data in your React app, but it’s no fun if you can’t edit it!

Add a React GroupEdit component

Create app/src/GroupEdit.js and use useEffect() to fetch the group resource with the ID from the URL.

import React, { useEffect, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { Button, Container, Form, FormGroup, Input, Label } from 'reactstrap';
import AppNavbar from './AppNavbar';

const GroupEdit = () => {
  const initialFormState = {
    name: '',
    address: '',
    city: '',
    stateOrProvince: '',
    country: '',
    postalCode: ''
  };
  const [group, setGroup] = useState(initialFormState);
  const navigate = useNavigate();
  const { id } = useParams();

  useEffect(() => {
    if (id !== 'new') {
      fetch(`/api/group/${id}`)
        .then(response => response.json())
        .then(data => setGroup(data));
    }
  }, [id, setGroup]);

  const handleChange = (event) => {
    const { name, value } = event.target

    setGroup({ ...group, [name]: value })
  }

  const handleSubmit = async (event) => {
    event.preventDefault();

    await fetch(`/api/group${group.id ? `/${group.id}` : ''}`, {
      method: (group.id) ? 'PUT' : 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(group)
    });
    setGroup(initialFormState);
    navigate('/groups');
  }

  const title = <h2>{group.id ? 'Edit Group' : 'Add Group'}</h2>;

  return (<div>
      <AppNavbar/>
      <Container>
        {title}
        <Form onSubmit={handleSubmit}>
          <FormGroup>
            <Label for="name">Name</Label>
            <Input type="text" name="name" id="name" value={group.name || ''}
                   onChange={handleChange} autoComplete="name"/>
          </FormGroup>
          <FormGroup>
            <Label for="address">Address</Label>
            <Input type="text" name="address" id="address" value={group.address || ''}
                   onChange={handleChange} autoComplete="address-level1"/>
          </FormGroup>
          <FormGroup>
            <Label for="city">City</Label>
            <Input type="text" name="city" id="city" value={group.city || ''}
                   onChange={handleChange} autoComplete="address-level1"/>
          </FormGroup>
          <div className="row">
            <FormGroup className="col-md-4 mb-3">
              <Label for="stateOrProvince">State/Province</Label>
              <Input type="text" name="stateOrProvince" id="stateOrProvince" value={group.stateOrProvince || ''}
                     onChange={handleChange} autoComplete="address-level1"/>
            </FormGroup>
            <FormGroup className="col-md-5 mb-3">
              <Label for="country">Country</Label>
              <Input type="text" name="country" id="country" value={group.country || ''}
                     onChange={handleChange} autoComplete="address-level1"/>
            </FormGroup>
            <FormGroup className="col-md-3 mb-3">
              <Label for="country">Postal Code</Label>
              <Input type="text" name="postalCode" id="postalCode" value={group.postalCode || ''}
                     onChange={handleChange} autoComplete="address-level1"/>
            </FormGroup>
          </div>
          <FormGroup>
            <Button color="primary" type="submit">Save</Button>{' '}
            <Button color="secondary" tag={Link} to="/groups">Cancel</Button>
          </FormGroup>
        </Form>
      </Container>
    </div>
  )
};

export default GroupEdit;

The useParams() hook is used to grab the ID from the URL and useNavigate() allows you to navigate back to the GroupList after adding or saving a group.

Modify app/src/App.js to import GroupEdit and specify a path to it.

import GroupEdit from './GroupEdit';

const App = () => {
  return (
    <Router>
      <Routes>
        ...
        <Route path='/groups/:id' element={<GroupEdit/>}/>
      </Routes>
    </Router>
  )
}

Now you should be able to add and edit groups!

Add Group screen

Edit Group screen

Add authentication with Okta

It’s pretty cool to build a CRUD app, but it’s even cooler to build a secure one. To achieve that, you’ll want to add authentication so users have to log in before viewing/modifying groups. To make this simple, you can use Okta’s API for OIDC. At Okta, our goal is to make identity management a lot easier, more secure, and more scalable than what you’re used to. Okta is a cloud service that allows developers to create, edit, and securely store user accounts and user account data, and connect them with one or multiple applications. Our API enables you to:

Are you sold? Register for a forever-free developer account, and when you’re done, come on back so you can learn more about building secure apps with Spring Boot!

Spring Security + OIDC

Spring Security added OIDC support in its 5.0 release. Since then, they’ve made quite a few improvements and simplified its required configuration. Add the Okta Spring Boot starter to do OIDC authentication.

<dependency>
    <groupId>com.okta.spring</groupId>
    <artifactId>okta-spring-boot-starter</artifactId>
    <version>2.1.6</version>
</dependency>

This dependency is a thin wrapper around Spring Security’s OAuth and encapsulates the following dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

Create an OIDC app in Okta

Install the Okta CLI and run okta login. Then, run okta apps create. Select the default app name, or change it as you see fit. Choose Web and press Enter.

Select Okta Spring Boot Starter. Then, change the Redirect URI to http://localhost:8080/login/oauth2/code/okta and use http://localhost:3000,http://localhost:8080 for the Logout Redirect URI.

What does the Okta CLI do?

Okta application configuration has been written to: 
  /path/to/app/src/main/resources/application.properties

Open src/main/resources/application.properties to see the issuer and credentials for your app.

okta.oauth2.issuer=https://dev-133337.okta.com/oauth2/default
okta.oauth2.client-id=0oab8eb55Kb9jdMIr5d6
okta.oauth2.client-secret=NEVER-SHOW-SECRETS

Use Auth0 for OIDC

If you’d rather use Auth0, that’s possible too! First, you’ll need to use the Spring Security dependencies as mentioned above. The Okta Spring Boot starter currently doesn’t work with Auth0.

Then, install the Auth0 CLI and run auth0 login in a terminal.

Next, run auth0 apps create, provide a memorable name, and select Regular Web Application. Specify http://localhost:8080/login/oauth2/code/auth0 for the Callback URLs and http://localhost:3000,http://localhost:8080 for the Allowed Logout URLs.

Modify your src/main/resources/application.properties to include your Auth0 issuer, client ID, and client secret. You will have to run auth0 apps open and select the app you created to copy your client secret.

# make sure to include the trailing slash for the Auth0 issuer
spring.security.oauth2.client.provider.auth0.issuer-uri=https://<your-auth0-domain>/
spring.security.oauth2.client.registration.auth0.client-id=<your-client-id>
spring.security.oauth2.client.registration.auth0.client-secret=<your-client-secret>
spring.security.oauth2.client.registration.auth0.scope=openid,profile,email

Of course, you can also use your Auth0 dashboard to configure your application. Just make sure to use the same URLs specified above.

After configuring Spring Security in the section below, update UserController.java to use auth0 in its constructor:

public UserController(ClientRegistrationRepository registrations) {
    this.registration = registrations.findByRegistrationId("auth0");
}

And update its logout() method to work with Auth0:

@PostMapping("/api/logout")
public ResponseEntity<?> logout(HttpServletRequest request) {
    // send logout URL to client so they can initiate logout
    StringBuilder logoutUrl = new StringBuilder();
    String issuerUri = this.registration.getProviderDetails().getIssuerUri();
    logoutUrl.append(issuerUri.endsWith("/") ? issuerUri + "v2/logout" : issuerUri + "/v2/logout");
    logoutUrl.append("?client_id=").append(this.registration.getClientId());

    Map<String, String> logoutDetails = new HashMap<>();
    logoutDetails.put("logoutUrl", logoutUrl.toString());
    request.getSession(false).invalidate();
    return ResponseEntity.ok().body(logoutDetails);
}

You’ll also need to update Home.js in the React project to use different parameters for the logout redirect:

window.location.href = `${response.logoutUrl}&returnTo=${window.location.origin}`;

You can see all the differences between Okta and Auth0 by comparing their branches on GitHub.

Configure Spring Security for React and user identity

To make Spring Security React-friendly, create a SecurityConfiguration.java file in src/main/java/.../jugtours/config. Create the config directory and put this class in it.

package com.okta.developer.jugtours.config;

import com.okta.developer.jugtours.web.CookieCsrfFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SimpleSavedRequest;

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .requestMatchers("/", "/index.html", "/static/**",
                    "/*.ico", "/*.json", "/*.png", "/api/user").permitAll()
                .anyRequest().authenticated()
            )
            .csrf((csrf) -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
            )
            .addFilterAfter(new CookieCsrfFilter(), BasicAuthenticationFilter.class)
            .oauth2Login();
        return http.build();
    }

    @Bean
    public RequestCache refererRequestCache() { 
        return new HttpSessionRequestCache() {
            @Override
            public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
                String referrer = request.getHeader("referer");
                if (referrer == null) {
                    referrer = request.getRequestURL().toString();
                }
                request.getSession().setAttribute("SPRING_SECURITY_SAVED_REQUEST",
                    new SimpleSavedRequest(referrer));

            }
        };
    }
}

This class has a lot going on, so let me explain a few things. In previous versions of Spring Security, there was an authorizeRequests() lambda you could use to secure paths. It still exists, but it’s deprecated in Spring Security 6. It’s permissive by default, which means any paths you don’t specify will be allowed. The recommended way, shown here with authorizeHttpRequests() denies by default. This means you have to specify the resources you want to allow Spring Security to serve up, as well as the ones that the React app has.

The requestMatchers lines defines what URLs are allowed for anonymous users. You will soon configure things so your React app is served up by your Spring Boot app, hence the reason for allowing “/”, “/index.html”, and web files. You might also notice an exposed /api/user path.

The RequestCache bean overrides the default request cache. It saves the referrer header (misspelled referer in real life), so Spring Security can redirect back to it after authentication. The referrer-based request cache comes in handy when you’re developing React on http://localhost:3000 and want to be redirected back there after logging in.

Configuring CSRF (cross-site request forgery) protection with CookieCsrfTokenRepository.withHttpOnlyFalse() means that the XSRF-TOKEN cookie won’t be marked HTTP-only, so React can read it and send it back when it tries to manipulate data. The CsrfTokenRequestAttributeHandler is no longer the default, so you have to configure it as the request handler. You can read this Stack Overflow answer to learn more. Basically, since we’re not sending the CSRF token to an HTML page, we don’t have to worry about BREACH attacks. This means we can revert to the previous default from Spring Security 5.

You’ll need to create the CookieCsrfFilter class that’s added because Spring Security 6 no longer sets the cookie for you. Create it in the web package.

package com.okta.developer.jugtours.web;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

/**
 * Spring Security 6 doesn't set a XSRF-TOKEN cookie by default.
 * This solution is
 * <a href="https://github.com/spring-projects/spring-security/issues/12141#issuecomment-1321345077">
 * recommended by Spring Security.</a>
 */
public class CookieCsrfFilter extends OncePerRequestFilter {

  /**
   * {@inheritDoc}
   */
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                  FilterChain filterChain) throws ServletException, IOException {
    CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
    response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
    filterChain.doFilter(request, response);
  }
}

Create src/main/java/.../jugtours/web/UserController.java and populate it with the following code. This API will be used by React to 1) find out if a user is authenticated, and 2) perform global logout.

package com.okta.developer.jugtours.web;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@RestController
public class UserController {
    private ClientRegistration registration;

    public UserController(ClientRegistrationRepository registrations) {
        this.registration = registrations.findByRegistrationId("okta");
    }

    @GetMapping("/api/user")
    public ResponseEntity<?> getUser(@AuthenticationPrincipal OAuth2User user) {
        if (user == null) {
            return new ResponseEntity<>("", HttpStatus.OK);
        } else {
            return ResponseEntity.ok().body(user.getAttributes());
        }
    }

    @PostMapping("/api/logout")
    public ResponseEntity<?> logout(HttpServletRequest request,
                                    @AuthenticationPrincipal(expression = "idToken") OidcIdToken idToken) {
        // send logout URL to client so they can initiate logout
        String logoutUrl = this.registration.getProviderDetails()
                .getConfigurationMetadata().get("end_session_endpoint").toString();

        Map<String, String> logoutDetails = new HashMap<>();
        logoutDetails.put("logoutUrl", logoutUrl);
        logoutDetails.put("idToken", idToken.getTokenValue());
        request.getSession(false).invalidate();
        return ResponseEntity.ok().body(logoutDetails);
    }
}

You’ll also want to add user information when creating groups so that you can filter by your JUG tour. Add a UserRepository.java in the same directory as GroupRepository.java.

package com.okta.developer.jugtours.model;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, String> {
}

Add a new findAllByUserId(String id) method to GroupRepository.java.

List<Group> findAllByUserId(String id);

Then inject UserRepository into GroupController.java and use it to create (or grab an existing user) when adding a new group. While you’re there, modify the groups() method to filter by user.

package com.okta.developer.jugtours.web;

import com.okta.developer.jugtours.model.Group;
import com.okta.developer.jugtours.model.GroupRepository;
import com.okta.developer.jugtours.model.User;
import com.okta.developer.jugtours.model.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;

@RestController
@RequestMapping("/api")
class GroupController {

    private final Logger log = LoggerFactory.getLogger(GroupController.class);
    private GroupRepository groupRepository;
    private UserRepository userRepository;

    public GroupController(GroupRepository groupRepository, UserRepository userRepository) {
        this.groupRepository = groupRepository;
        this.userRepository = userRepository;
    }

    @GetMapping("/groups")
    Collection<Group> groups(Principal principal) {
        return groupRepository.findAllByUserId(principal.getName());
    }

    @GetMapping("/group/{id}")
    ResponseEntity<?> getGroup(@PathVariable Long id) {
        Optional<Group> group = groupRepository.findById(id);
        return group.map(response -> ResponseEntity.ok().body(response))
                .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }

    @PostMapping("/group")
    ResponseEntity<Group> createGroup(@Valid @RequestBody Group group,
                                      @AuthenticationPrincipal OAuth2User principal) throws URISyntaxException {
        log.info("Request to create group: {}", group);
        Map<String, Object> details = principal.getAttributes();
        String userId = details.get("sub").toString();
 
        // check to see if user already exists
        Optional<User> user = userRepository.findById(userId);
        group.setUser(user.orElse(new User(userId,
                        details.get("name").toString(), details.get("email").toString())));

        Group result = groupRepository.save(group);
        return ResponseEntity.created(new URI("/api/group/" + result.getId()))
                .body(result);
    }

    @PutMapping("/group/{id}")
    ResponseEntity<Group> updateGroup(@Valid @RequestBody Group group) {
        log.info("Request to update group: {}", group);
        Group result = groupRepository.save(group);
        return ResponseEntity.ok().body(result);
    }

    @DeleteMapping("/group/{id}")
    public ResponseEntity<?> deleteGroup(@PathVariable Long id) {
        log.info("Request to delete group: {}", id);
        groupRepository.deleteById(id);
        return ResponseEntity.ok().build();
    }
}

To magnify the changes, they’re in the groups() and createGroup() methods. It’s pretty slick that Spring JPA will create the findAllByUserId() method/query for you and userRepository.findById() uses Java 8’s Optional.

@GetMapping("/groups")
Collection<Group> groups(Principal principal) {
    return groupRepository.findAllByUserId(principal.getName());
}

@PostMapping("/group")
ResponseEntity<Group> createGroup(@Valid @RequestBody Group group,
                                  @AuthenticationPrincipal OAuth2User principal) throws URISyntaxException {
    log.info("Request to create group: {}", group);
    Map<String, Object> details = principal.getAttributes();
    String userId = details.get("sub").toString();

    // check to see if user already exists
    Optional<User> user = userRepository.findById(userId);
    group.setUser(user.orElse(new User(userId,
                    details.get("name").toString(), details.get("email").toString())));

    Group result = groupRepository.save(group);
    return ResponseEntity.created(new URI("/api/group/" + result.getId()))
            .body(result);
}

Modify React to handle CSRF and be identity-aware

You’ll need to make a few changes to your React components to make them identity-aware. The first thing you’ll want to do is modify src/index.js to wrap everything in a CookieProvider. This component allows you to read the CSRF cookie and send it back as a header.

import { CookiesProvider } from 'react-cookie';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <CookiesProvider>
      <App/>
    </CookiesProvider>
  </React.StrictMode>
);

Modify app/src/Home.js to call /api/user to see if the user is logged in. If they’re not, show a Login button.

import React, { useEffect, useState } from 'react';
import './App.css';
import AppNavbar from './AppNavbar';
import { Link } from 'react-router-dom';
import { Button, Container } from 'reactstrap';
import { useCookies } from 'react-cookie';

const Home = () => {

  const [authenticated, setAuthenticated] = useState(false);
  const [loading, setLoading] = useState(false);
  const [user, setUser] = useState(undefined);
  const [cookies] = useCookies(['XSRF-TOKEN']);

  useEffect(() => {
    setLoading(true);
    fetch('api/user', { credentials: 'include' })
      .then(response => response.text())
      .then(body => {
        if (body === '') {
          setAuthenticated(false);
        } else {
          setUser(JSON.parse(body));
          setAuthenticated(true);
        }
        setLoading(false);
      });
  }, [setAuthenticated, setLoading, setUser])

  const login = () => {
    let port = (window.location.port ? ':' + window.location.port : '');
    if (port === ':3000') {
      port = ':8080';
    }
    // redirect to a protected URL to trigger authentication
    window.location.href = `//${window.location.hostname}${port}/api/private`;
  }

  const logout = () => {
    fetch('/api/logout', {
      method: 'POST', credentials: 'include',
      headers: { 'X-XSRF-TOKEN': cookies['XSRF-TOKEN'] }
    })
      .then(res => res.json())
      .then(response => {
        window.location.href = `${response.logoutUrl}?id_token_hint=${response.idToken}`
          + `&post_logout_redirect_uri=${window.location.origin}`;
      });
  }

  const message = user ?
    <h2>Welcome, {user.name}!</h2> :
    <p>Please log in to manage your JUG Tour.</p>;

  const button = authenticated ?
    <div>
      <Button color="link"><Link to="/groups">Manage JUG Tour</Link></Button>
      <br/>
      <Button color="link" onClick={logout}>Logout</Button>
    </div> :
    <Button color="primary" onClick={login}>Login</Button>;

  if (loading) {
    return <p>Loading...</p>;
  }

  return (
    <div>
      <AppNavbar/>
      <Container fluid>
        {message}
        {button}
      </Container>
    </div>
  );
}

export default Home;

There are some things you should be aware of in this component:

  1. useCookies() is used for access to cookies. Then you can fetch a cookie with cookies['XSRF-TOKEN'].
  2. When using fetch(), you need to include {credentials: 'include'} to transfer cookies. You will get a 403 Forbidden if you do not include this option.
  3. The CSRF cookie from Spring Security has a different name than the header you need to send back. The cookie name is XSRF-TOKEN, while the header name is X-XSRF-TOKEN.

Update app/src/GroupList.js to have similar changes. The good news is you don’t need to make any changes to the render() method.

import { useCookies } from 'react-cookie';

const GroupList = () => {

  ...
  const [cookies] = useCookies(['XSRF-TOKEN']);

  ...
  const remove = async (id) => {
    await fetch(`/api/group/${id}`, {
      method: 'DELETE',
      headers: {
        'X-XSRF-TOKEN': cookies['XSRF-TOKEN'],
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      credentials: 'include'
    }).then(() => {
      let updatedGroups = [...groups].filter(i => i.id !== id);
      setGroups(updatedGroups);
    });
  }
  ...

  return (...)
}

export default GroupList;

Update GroupEdit.js too.

import { useCookies } from 'react-cookie';

const GroupEdit = () => {
  
  ...
  const [cookies] = useCookies(['XSRF-TOKEN']);

  ...
  const handleSubmit = async (event) => {
    event.preventDefault();

    await fetch(`/api/group${group.id ? `/${group.id}` : ''}`, {
      method: group.id ? 'PUT' : 'POST',
      headers: {
        'X-XSRF-TOKEN': cookies['XSRF-TOKEN'],
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(group),
      credentials: 'include'
    });
    setGroup(initialFormState);
    navigate('/groups');
  }

  ...

  return (...)
}

export default GroupEdit;

After all these changes, you should be able to restart both Spring Boot and React and witness the glory of planning your very own JUG Tour!

React Login

My JUG Tour

Configure Maven to build and package React with Spring Boot

To build and package your React app with Maven, you can use the frontend-maven-plugin and Maven’s profiles to activate it. Add properties for versions and a <profiles> section to your pom.xml.

<properties>
    ...
    <frontend-maven-plugin.version>1.12.1</frontend-maven-plugin.version>
    <node.version>v16.18.1</node.version>
    <npm.version>v8.19.2</npm.version>
</properties>

<profiles>
    <profile>
        <id>dev</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <spring.profiles.active>dev</spring.profiles.active>
        </properties>
    </profile>
    <profile>
        <id>prod</id>
        <build>
            <plugins>
                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>copy-resources</id>
                            <phase>process-classes</phase>
                            <goals>
                                <goal>copy-resources</goal>
                            </goals>
                            <configuration>
                                <outputDirectory>${basedir}/target/classes/static</outputDirectory>
                                <resources>
                                    <resource>
                                        <directory>app/build</directory>
                                    </resource>
                                </resources>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>com.github.eirslett</groupId>
                    <artifactId>frontend-maven-plugin</artifactId>
                    <version>${frontend-maven-plugin.version}</version>
                    <configuration>
                        <workingDirectory>app</workingDirectory>
                    </configuration>
                    <executions>
                        <execution>
                            <id>install node</id>
                            <goals>
                                <goal>install-node-and-npm</goal>
                            </goals>
                            <configuration>
                                <nodeVersion>${node.version}</nodeVersion>
                                <npmVersion>${npm.version}</npmVersion>
                            </configuration>
                        </execution>
                        <execution>
                            <id>npm install</id>
                            <goals>
                                <goal>npm</goal>
                            </goals>
                            <phase>generate-resources</phase>
                        </execution>
                        <execution>
                            <id>npm test</id>
                            <goals>
                                <goal>test</goal>
                            </goals>
                            <phase>test</phase>
                            <configuration>
                                <arguments>test</arguments>
                                <environmentVariables>
                                    <CI>true</CI>
                                </environmentVariables>
                            </configuration>
                        </execution>
                        <execution>
                            <id>npm build</id>
                            <goals>
                                <goal>npm</goal>
                            </goals>
                            <phase>compile</phase>
                            <configuration>
                                <arguments>run build</arguments>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
        <properties>
            <spring.profiles.active>prod</spring.profiles.active>
        </properties>
    </profile>
</profiles>

While you’re at it, add the active profile setting to src/main/resources/application.properties:

spring.profiles.active=@spring.profiles.active@

After adding this, you should be able to run ./mvnw spring-boot:run -Pprod and see your app running on http://localhost:8080.

App Running with Maven

Everything will work just fine if you start at the root, since React will handle routing. However, if you refresh the page when you’re at http://localhost:8080/groups, you’ll get a 404 error since Spring Boot doesn’t have a route for /groups. To fix this, add a SpaWebFilter that conditionally forwards to the React app.

package com.okta.developer.jugtours.web;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.security.Principal;

public class SpaWebFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String path = request.getRequestURI();
        Authentication user = SecurityContextHolder.getContext().getAuthentication();
        if (user != null && !path.startsWith("/api") && !path.contains(".") && path.matches("/(.*)")) {
            request.getRequestDispatcher("/").forward(request, response);
            return;
        }

        filterChain.doFilter(request, response);
    }
}

And, add it to SecurityConfiguration.java:

.addFilterAfter(new SpaWebFilter(), BasicAuthenticationFilter.class)

Now, if you restart and reload the page, everything will work as expected. 🤗


Original article source at https://developer.okta.com 

#react #springboot

 

How to Create a CRUD App with Spring Boot and React
Bongani  Ngema

Bongani Ngema

1672066691

How to Reliable Database Migrations with Liquibase and Spring Boot

In this blog, we look at database migrations with the very popular Liquibase database migration library and how you can use it in the context of a Spring Boot application.

Setting Up Liquibase in Spring Boot

By using default Spring Boot auto-configures Liquibase while we upload the Liquibase dependency to our construct document.
Spring Boot makes use of the primary DataSource to run Liquibase (i.e. the only annotated with @primary if there is a couple of). In case we need to apply a special DataSource we can mark that bean as @LiquibaseDataSource.
rather, we are able to set the spring. liquibase.[URL, user, password]properties, in order that spring creates a Datasource on its personal and uses it to automobile-configure Liquibase.

Through default, Spring Boot runs Liquibase database migrations mechanically on application startup.

It looks for a grasp changelog report in the folder DB/migration within the classpath with the name DB.changelog-master. YAML. If we need to use other Liquibase changelog codecs or use distinctive report naming conventions, we are able to configure the spring. liquibase. exchange-log application property to point to an exclusive grasp changelog record.

for example, to use db/migration/db.change-log.json because of the master changelog document, we set the following assets in application.yml:


liquibase.change-log=classpath:db/migration/db.changelog-master.xml

The master changelog can consist of different changelogs so that we can break up our modifications into logical steps.

Running Our First Database Migration

After setting the whole lot up, permit’s create our first database migration. We’ll create the database desk user_details in this example.

Let’s initiate a file with the term db.changelog-master.yaml and place it in src/main/resources/db/changelog:


<databaseChangeLog
   
   <include file="db/changelog/db.changelog-1.0.0.xml"/> 
                  
 </databaseChangeLog>

The master file is simply a cluster of includes that implies changelogs with the authentic changes.

Next, we build the changelog with the first real changeset and placed it into the file src/main/resources/db/changelog-yaml-example.yaml:

 
<databaseChangeLog>

<changeSet author="Krishna (generated)" id="1503460396396-1">

		<createTable tableName="employee_table">

			<column autoIncrement="true" name="employee_id" type="INT">
				<constraints primaryKey="true" />

			</column>

			<column name="email" type="VARCHAR(255)" />

			<column name="employee_name" type="VARCHAR(255)" />

			<column name="salary" type="DOUBLE" />

		</createTable>

	</changeSet>
	
	<changeSet author="Kishank (generated)" id="1503460396396-2">

		<createIndex indexName="EMAIL_INDEX" 

tableName="employee_table">

			<column name="email" />

		</createIndex>

	</changeSet>
	
	<changeSet id="1203460396356-3" author="Krishna (generated)">

		<insert tableName="employee_table">

			<column name="employee_name" value="Deepak"></column>

			<column name="email" value="rock.deep@yahoo.com"></column>
			<column name="salary" valueNumeric="85000.00"></column>
		</insert>

		<insert tableName="employee_table">

			<column name="employee_name" value="Manish"></column>

			<column name="email" value="manish.s@yahoo.com"></column>
			<column name="salary" valueNumeric="90000.00">

</column>
		</insert>

	</changeSet>

</databaseChangeLog>

Using Liquibase Context

As defined earlier, context can be used to control which exchange units have to run. let’s use this to add take a look at the information in the check and local environments:

<databaseChangeLog>

 <changeSet 

   author="Krishna Jaiswal" 

   id="1503460396396-1" 

   context="test or local">

   <loadUpdateData

     encoding="UTF-8"

     file="db/data/employee.csv"

     onlyUpdate="false"

     primaryKey="id"

     quotchar="'"

     separator=","

     tableName="emp_details">

   </loadUpdateData>

 </changeSet>

</databaseChangeLog>

We’re using the expression test or local so it runs for these contexts, but not in production.

So now we need to pass the context to Liquibase using the property spring.liquibase.contexts:

spring.profiles: docker

liquibase.parameters.textColumnType: TEXT

 contexts: test

Creating migration file

To create a new migration, you could run the make: migration Artisan command, and on the way bootstrap a new class to your Laravel utility, in the database/migrations folder.

 create table employee_table (

        employee_id integer not null auto_increment,

        email varchar(255),

        employee_name varchar(255),

        salary double precision,

        primary key (employee_id)

  );

Enabling Logging for Liquibase in Spring Boot

Allowing info degree logging for Liquibase will assist to peer the trade sets that Liquibase executes for the duration of the beginning of the application. It additionally enables us to identify that the utility has no longer commenced yet due to the fact it’s miles waiting to collect a changelog lock at some stage in the startup.
upload the subsequent application belongings in application.yml to allow info logs:

logging:

  level:

    "liquibase" : info

Conclusion

Liquibase enables automating database migrations, and Spring Boot makes it less complicated to apply Liquibase. This manual furnished info on a way to use Liquibase in the Spring Boot software and a few best practices.

Reference

https://blog.nimbleways.com/db-migrations-with-liquibase-and-spring-boot/

You can find the example code on GitHub.

Original article source at: https://blog.knoldus.com/

#springboot #liquibase #database 

How to Reliable Database Migrations with Liquibase and Spring Boot
Bongani  Ngema

Bongani Ngema

1672062840

How to Evolving Database using Spring Boot and Liquibase

Introduction

In this Blog, we will see an example of evolving database using Spring Boot and Liquibase with YAML and SQL configuration. Here we will learn how to build applications using maven build tools.

Liquibase is an open-source library for tracking, managing, and deploying database changes that can be used for any database. It helps you create the schema, run them during deployment, and also helps you write automated tests so that your changes will work in production.

Create Project

We need to create maven based project.

If you are creating maven based project then add the following dependencies.

Create Changelog File

Create a YAML file called db.changelog-master.yaml under the src/main/resources/db folder. It will cover all the changelogs written in different files. The complete master changelog file content is given below:

Notice in the above YAML file, we have not specified any endDelimiter for changeSet ids insertTableAddresses and insertTableUsers because the default end delimiter is ;.

Now create below SQL file 01-create-users-and-addresses-schema.sql under src/main/resources/db/changelog/scripts to create tables in the MySQL database:

NowcreatebelowSQLfile 02-insert-data-addresses.sql under src/main/resources/db/changelog/scripts folder to insert data into the ADDRESSES table:

Now create below SQL file 02-insert-data-users.sql under src/main/resources/db/changelog/scripts to insert data into the USERS table:

Application Properties

Create file src/main/resource/application.properties to load the database changelog file during Spring Boot application startup. Here we configure the database connection in this file.

The default location of the changelog master file is classpath:/db/changelog and Liquibase searches for a file db.changelog-master.yaml. So we need to declare the location of the changelog-master file.

In the Spring Boot application the key liquibase.change-log does not work, so you need to use spring.liquibase.changeLog.

Create Main Class

Here is the main application in order to start the application and the creation and insertion of the above table into the database will be occurring during the application execution.

Testing the Application

After running the project it will have the following output:

Conclusion

You will find tables created in the database. The two rows are inserted into addresses and two rows are inserted into users’ tables. Also, there are three rows inserted into the table DATABASECHANGELOG and the row identifies all details about the executed file. We will also find one row inserted into the table DATABASECHANGELOGLOCK and this row identifies whether the current operation holds a lock on changesets or not.

Original article source at: https://blog.knoldus.com/

#springboot #liquibase #database 

How to Evolving Database using Spring Boot and Liquibase

How to implement Event Sourcing with SpringBoot

The name directly comes from the fact that event sourcing events are the source of truth. So all of the other data and other data structures are just derived from the events. So we can erase in theory all of those other storages as long as we keep event lock then we can always regenerate them. Event sourcing contains a ordered of our operation so if we have look on the shopping cart.

  • At first we are initializing the shopping cart.
  • We are adding new product.
  • We may remove the product because we decided that we did it by mistake.
  • Then We added a new product.
  • At the end we are confirming card.

Nice thing about Event sourcing is that we are able to do time traveling. If we have recorded the sequence of events then we can always go back, So we can just take the events and apply that to the current state and get back to time to see what has happened.

Use case :

Let’s take a use case. Gaurav is a shop keeper, he sells electronic items like mobile phones, laptops etc, he wants to keep track of stock in his shop and wants to know whether his shop has stock of a particular item or not without checking manually. He wants an app for it.

The app has three functionalities:

  • User can add new stock.
  • He can remove stock after selling it.
  • User can find the current stock of a particular item.

event-sourcing

In Event Sourcing you just capture user events and add them in database, you just keep adding new events for every user action and no record is updated or deleted in the database , just events are added. With events, you also add event data specific to the event.

In this way you maintain the history of the user action. It is useful if your application has security requirements to audit all user actions. This is also useful in any application where you want a history of user actions (eg Github commits, analytics applications, etc.) and to know the current state of an entity, you simply iterate through your code. are and receive it.

The project structure will be as follows-

project-structure

The pom.xml 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>stockmanagement_eventstore</artifactId>
	<version>1.0.0</version>
	<name>stockmanagement_eventstore</name>
	<description>Demo project for Event Sourcing</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- H2 database dependency(in-memory databases ) -->
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    
      </dependency>
      <dependency>
	     <groupId>org.springframework.boot</groupId>
	     <artifactId>spring-boot-starter-test</artifactId>
	     <scope>test</scope>
     </dependency>
     <!-- Lombok remove boilerplate codes -->
     <dependency>
	     <groupId>org.projectlombok</groupId>
	     <artifactId>lombok</artifactId>
	     <optional>true</optional>
    </dependency>
</dependencies>

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

</project>

Created Stock Model class

package com.example.stock.management;

import lombok.Data;

//entity model
@Data
public class Stock {

	private String name;
	private int quantity;
	private String user;
	
}

EventStore class will be as follows

package com.example.stock.management;

import java.time.LocalDateTime;
import java.util.Map;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import lombok.Builder;
import lombok.Data;

@Entity
@Data

public class EventStore {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long eventId;

	private String eventType;

	private String entityId;
	
	private String eventData;

	private LocalDateTime eventTime;

}

Created StockEvent interface

package com.example.stock.management;

public interface StockEvent {

}

Here is StockAddedEvent class and it’s implementaion

package com.example.stock.management;

import lombok.Builder;
import lombok.Data;

@Builder
@Data
public class StockAddedEvent implements StockEvent {

	private Stock stockDetails;
	
}

Created StockRemovedEvent class and it’s implementaion

package com.example.stock.management;

import lombok.Builder;
import lombok.Data;

@Builder
@Data
public class StockRemovedEvent implements StockEvent {
	
	private Stock stockDetails;
}

Added EventRepository class

package com.example.stock.management;

import java.time.LocalDateTime;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;

@Component
public interface EventRepository extends CrudRepository<EventStore, Long>{

	Iterable<EventStore> findByEntityId(String entityId);
	
	Iterable<EventStore> findByEntityIdAndEventTimeLessThanEqual(String entityId,LocalDateTime date);

}

Created EventService class

package com.example.stock.management;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@Service
public class EventService {

	@Autowired
	private EventRepository repo;

	public void addEvent(StockAddedEvent evnt) throws JsonProcessingException {

		EventStore eventStore = new EventStore();
		eventStore.setEventData(new ObjectMapper().writeValueAsString(event.getStockDetails()));
		eventStore.setEventType("STOCK_ADDED");
		eventStore.setEntityId(event.getStockDetails().getName());
		eventStore.setEventTime(LocalDateTime.now());
		repo.save(eventStore);
	}

	public void addEvent(StockRemovedEvent event) throws JsonProcessingException {

		EventStore eventStore = new EventStore();
		eventStore.setEventData(new ObjectMapper().writeValueAsString(event.getStockDetails()));
		eventStore.setEventType("STOCK_REMOVED");
		eventStore.setEntityId(event.getStockDetails().getName());
		eventStore.setEventTime(LocalDateTime.now());
		repo.save(eventStore);
	}

	public Iterable<EventStore> fetchAllEvents(String name) {

		return repo.findByEntityId(name);

	}
	
	public Iterable<EventStore> fetchAllEventsTillDate(String name,LocalDateTime date) {

		return repo.findByEntityIdAndEventTimeLessThanEqual(name, date);

	}
}

Created StockController class for adding a stock item , removing a stock item and Getting current count of stock. 

package com.example.stock.management;

import java.time.LocalDate;
import java.time.LocalDateTime;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.gson.Gson;

@RestController
public class StockController {

	@Autowired
	private EventService service;

	// Adding a stock item
	@PostMapping("/stock")
	public void addStock(@RequestBody Stock stockRequest) throws JsonProcessingException {

		StockAddedEvent event = StockAddedEvent.builder().stockDetails(stockRequest).build();
		service.addEvent(event);
	}

	// To remove item from a stock
	@DeleteMapping("/stock")
	public void removeStock(@RequestBody Stock stock) throws JsonProcessingException {

		StockRemovedEvent event = StockRemovedEvent.builder().stockDetails(stock).build();
		service.addEvent(event);
	}

	//To get current count of stock
	@GetMapping("/stock")
	public Stock getStock(@RequestParam("name") String name) throws JsonProcessingException {

		Iterable<EventStore> events = service.fetchAllEvents(name);

		Stock currentStock = new Stock();
		currentStock.setName(name);
		currentStock.setUser("NA");

		for (EventStore event : events) {

			Stock stock = new Gson().fromJson(event.getEventData(), Stock.class);

			if (event.getEventType().equals("STOCK_ADDED")) {

				currentStock.setQuantity(currentStock.getQuantity() + stock.getQuantity());
			} else if (event.getEventType().equals("STOCK_REMOVED")) {

				currentStock.setQuantity(currentStock.getQuantity() - stock.getQuantity());
			}
		}

		return currentStock;
	}
	
	@GetMapping("/events")
	public Iterable<EventStore> getEvents(@RequestParam("name") String name) throws JsonProcessingException {

		Iterable<EventStore> events = service.fetchAllEvents(name);

		return events;

	}
	
	@GetMapping("/stock/history")
	public Stock getStockUntilDate(@RequestParam("date") String date,@RequestParam("name") String name) throws JsonProcessingException {
	
		String[] dateArray = date.split("-");
		
		LocalDateTime dateTill = LocalDate.of(Integer.parseInt(dateArray[0]), Integer.parseInt(dateArray[1]), Integer.parseInt(dateArray[2])).atTime(23, 59);
		
		Iterable<EventStore> events = service.fetchAllEventsTillDate(name,dateTill);

		Stock currentStock = new Stock();

		currentStock.setName(name);
		currentStock.setUser("NA");

		for (EventStore event : events) {

			Stock stock = new Gson().fromJson(event.getEventData(), Stock.class);

			if (event.getEventType().equals("STOCK_ADDED")) {

				currentStock.setQuantity(currentStock.getQuantity() + stock.getQuantity());
			} else if (event.getEventType().equals("STOCK_REMOVED")) {

				currentStock.setQuantity(currentStock.getQuantity() - stock.getQuantity());
			}
		}

		return currentStock;

	}
}

StockmanagementEventstoreApplication class will be as follows

package com.example.stock.management;

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

// Main class of the application
@SpringBootApplication
public class StockmanagementEventstoreApplication {

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

}

Added application.yml file

spring:
  datasource:
    url:  jdbc:h2:mem:testdb
    driverClassName: org.h2.Driver
    username: sa
    password:
 
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    
  h2:
    console: 
      enabled: true 
      path: /h2

Work Flow

Start the StockmanagementEventstoreApplication app

Adding some items to stock:

stock-item1

stock-item2

stock-item3

Let’s check the database :

stock-database

We are able to get the current stock by hitting the GET API

get-stock-item-via-name

If we wants to know what was the stock the day before.

get-stock-item-via-date

Conclusion

Fetching the current state of an entity is not straightforward and is not scalable in event sourcing. This can be mitigated by taking snapshots of events at a particular time, compute the state of the entity for that snapshot at that time, store it somewhere and then only replay events that occurred after that snapshot time. For more, you can refer to the documentation: https://www.baeldung.com/cqrs-event-sourcing-java

Original article source at: https://blog.knoldus.com/

#event #springboot 

How to implement Event Sourcing with SpringBoot
Noemi  Hintz

Noemi Hintz

1671421967

How to Create Secure Ionic Apps with angular and JHipster

In this article about Angular, we will learn about How to Create Secure Ionic Apps with angular and JHipster. Ionic is a framework for building mobile apps with web technologies that look and act like native apps. Because they’re built with web technologies (HTML, JavaScript, and CSS), you can also deploy your Ionic apps as single-page applications. Or, even better, as progressive web apps (PWAs) that work offline.

Ionic supports the big three web frameworks: Angular, React, and Vue. Once you’ve written your app, you can deploy it to a simulator or device with Capacitor. Capacitor (pictured as the blue layer) provides the runtime for your app to communicate with the native operating system and vice versa.

Capacitor diagram

Ionic’s main competitors are native apps built with Swift or Objective-C (for iOS) and Java or Kotlin (for Android). Ionic also competes with React Native, which uses web technologies and translates them to native components.

The Ionic blog has a recent post that does a performance comparison between Ionic and React Native. TL;DR: Both options will give you a high-performance app with a truly native look and feel.

What the heck is JHipster?

This tutorial will show you how to use Ionic, Angular, and Capacitor to build a mobile app that talks to a Spring Boot backend. It won’t take but a few minutes, thanks to JHipster!

JHipster is an application generator that creates an Angular frontend and a Spring Boot backend based on the options you choose. It has the ability for you, as a developer, to customize what it generates with blueprints. The blueprints feature has resulted in many additional options for an app: Kotlin, Spring Native, Micronaut, Quarkus, .NET Core, NestJS, and Svelte.

Below is a diagram of the app you’ll create in this tutorial and its authentication flow.

JHipster Ionic OAuth 2.0 flow

✨ Introducing the JHipster Ionic blueprint!

The JHipster project has supported generating an Ionic app using a generator-jhipster-ionic module for the past several years. As the primary maintainer of this module, I’m proud to announce that it has been re-written as a blueprint, and it’s much easier to understand now. The previous module relied on the Ionic CLI, the base Angular starter, the Ionic JHipster starter, and custom code to glue it all together. Now, the source code is all contained in one project.

Marcelo Shima volunteered to do the conversion, and after a couple of months, I’m proud to say the JHipster Ionic blueprint is now available!

Ionic for @JHipster v8 is now available! There's lots to love in this release:

💙 Now available as a blueprint
🧪 Migrated from Protractor to Cypress
⭐️ @Auth0 support
🅰️ Upgraded to Angular 13 and Ionic 6

https://t.co/WQ6ZTsTkPP#ionic #jhipster #angular #springboot

— Matt Raible (@mraible) May 10, 2022

Here’s how to use it:

Create an ionic-app directory alongside your JHipster app.

- backend
- ionic-app

Navigate into ionic-app using your terminal. Install Ionic for JHipster and create a new app using jhipster-ionic.

npm install -g generator-jhipster-ionic
jhipster-ionic

You’ll be prompted for the location of your JHipster app, a name for your Ionic app, and then you’ll be off to the races!

JHipster Ionic prompts

You can also create a JHipster app and an Ionic app simultaneously by using the bundled JHipster.

mkdir bug-tracker && cd bug-tracker
jhipster-ionic jdl bug-tracker.jh
cd ../ionic4j

This process will follow the same convention where the generated backend and frontend apps are side-by-side on your hard drive.

Then you can run both apps from your Ionic app using easy-to-remember commands.

npm run backend:start
# open a new terminal window
npm start

Ionic serve command with backend running

 The JHipster Ionic blueprint currently only supports Angular. Now that it’s a blueprint, it will be much easier to add support for Vue and React. If you’re interested in helping out, please let me know! Okta is a platinum sponsor of the JHipster project and enjoys assigning bug bounties for feature development.

Build a mobile app with Ionic and Angular

To see Ionic + JHipster in action, let’s start with a Full Stack Java + React app I created for the Auth0 blog. I updated the app to the latest version of JHipster (v7.8.1) and created an Ionic app with JHipster Ionic, so everything is guaranteed to work. This Flickr clone allows you to upload photos, tag them, and organize them into albums. First, clone the example:

git clone https://github.com/oktadev/okta-jhipster-ionic-example.git \
  jhipster-ionic --depth 1
cd jhipster-ionic/backend

Start the app:

npm run ci:e2e:prepare # starts Keycloak and PostgreSQL in Docker
./mvnw

Then, navigate to http://localhost:8080 in your favorite browser. Sign in with admin/admin credentials and rejoice when it all works.

Open a new terminal window and enter the jhipster-ionic/ionic-app directory. Install its dependencies and run npm start to test the Ionic client.

npm install
npm start

You should be able to sign in and add a new photo.

Ionic welcomeIonic auth with Keycloak
Ionic home after log inHefe the Bus!

Please keep reading to learn how JHipster made all of this possible. Or, skip ahead to run your Ionic app on iOS using Capacitor.

How to integrate Ionic and Spring Boot

JHipster makes it easy to create a Spring Boot API that Spring Security protects. The JHipster Ionic blueprint generates an Ionic client that talks to your Spring Boot API and understands its auth mechanism. I created the jhipster-ionic project using the following steps:

Install the JHipster Ionic blueprint:

npm i -g generator-jhipster-ionic@8.0.0

Create a parent directory to hold everything:

# take is a shortcut for mdkir && cd
take jhipster-ionic

Clone an existing JHipster Flickr example:

git clone https://github.com/oktadev/auth0-full-stack-java-example.git backend --depth 1

Create a new directory to hold your Ionic project, then run jhipster-ionic in it:

take ionic-app
jhipster-ionic

Provide the path to your backend JHipster app and name your app flickr2.

JHipster Ionic with Flickr2 app

That’s it! The blueprint will generate an Ionic client, complete with screens for editing entities, unit tests, and end-to-end tests with Cypress.

Pretty slick, don’t you think?! 😎

Run your Spring Boot API

You’ll need to start your backend first, so your Ionic app can talk to its API. First, start Keycloak and PostgreSQL in Docker containers:

cd backend
npm run ci:e2e:prepare # starts Keycloak and PostgreSQL in Docker

Next, update backend/src/main/resources/config/application-prod.yml to allow CORS from http://localhost:8100.

jhipster:
  ...
  cors:
    allowed-origins: 'http://localhost:8100'
    allowed-methods: '*'
    allowed-headers: '*'
    exposed-headers: 'Authorization,Link,X-Total-Count,X-${jhipster.clientApp.name}-alert,X-${jhipster.clientApp.name}-error,X-${jhipster.clientApp.name}-params'
    allow-credentials: true
    max-age: 1800

Then, start the backend app using ./mvnw -Pprod. You should be able to log in at http://localhost:8080 (with admin/admin) and add new photos using Entities > Photos. Add a few photos so you have some data to work with.

Flickr2 photos

Run your Ionic app

Open another terminal and navigate to the ionic-app folder. Launch your Ionic client using npm start. In your default browser, the app will be opened at http://localhost:8100.

Ionic welcome

You should be able to log in with Keycloak and see all the listed entities in your app.

Ionic entities

In the JHipster app’s tutorial, there’s a section where you’re instructed to remove photo fields that can be calculated. Specifically, height, width, date taken, and date uploaded. These values are calculated when the photos are uploaded, so there’s no reason to display them when adding a photo.

To add this same functionality to your Ionic app, modify src/app/pages/entities/photo/photo-update.html and wrap these fields with <div *ngIf="!isNew">.

<div *ngIf="!isNew">
  <ion-item>
    <ion-label position="floating">Height</ion-label>
    <ion-input type="number" name="height" formControlName="height"></ion-input>
  </ion-item>
  ...
  <ion-item>
    <ion-label>Uploaded</ion-label>
    <ion-datetime displayFormat="MM/DD/YYYY HH:mm" formControlName="uploaded" id="field_uploaded"></ion-datetime>
  </ion-item>
</div>

The Ionic CLI will auto-compile and reload the app in your browser when you save this file. You can prove everything works as expected by stopping your app (with Ctrl+C) and running all the end-to-end tests with Cypress.

npm run e2e

Run your Ionic app on iOS using Capacitor

Generate a native iOS project with the following commands:

npx ionic build
npx ionic capacitor add ios

Add your custom scheme (dev.localhost.ionic) to ios/App/App/Info.plist. This scheme is configured in src/environments/environment.ts; you can easily change it to something else if you like.

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.getcapacitor.capacitor</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>capacitor</string>
      <string>dev.localhost.ionic</string>
    </array>
  </dict>
</array>

Modify the JHipster app’s CORS settings (in backend/src/main/resources/config/application-prod.yml) to allow capacitor://localhost as an origin.

jhipster:
  ...
  cors:
    allowed-origins: 'http://localhost:8100,capacitor://localhost'

Restart your backend app. Deploy your Ionic app to iOS Simulator and run it.

npx cap run ios

Confirm you can log in and rejoice in your success!

Flickr2 running on iOS

Run your Ionic app on Android

Generate an Android project with Capacitor.

npx ionic capacitor add android

Enable clear text traffic and add dev.localhost.ionic as a scheme in android/app/src/main/AndroidManifest.xml:

<activity ... android:usesCleartextTraffic="true">
  <!-- You'll need to add this intent filter so redirects work -->
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="dev.localhost.ionic" />
    <!--data android:scheme="com.okta.dev-133337" /-->
  </intent-filter>

  <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
</activity>

Modify the JHipster app’s CORS settings to allow http://localhost as an origin.

jhipster:
  ...
  cors:
    allowed-origins: 'http://localhost:8100,capacitor://localhost,http://localhost'

Restart your backend app and run your Ionic app on Android using the Capacitor CLI:

npx cap run android
 If you get an error when running this command, make sure to use Java 11.

You’ll need to run a couple of commands to allow the emulator to communicate with JHipster and Keycloak.

adb reverse tcp:8080 tcp:8080
adb reverse tcp:9080 tcp:9080

You should be able to log in and edit entities, just like you can in a browser and on iOS!

Flickr2 running on Android

Why use OpenID Connect for mobile apps?

Storing API keys and secrets in mobile apps is not safe. OAuth 2.0 solves this problem by not shipping any secrets in mobile apps and instead involving the user in the process of getting an access token into the app. These access tokens are unique per user, and they’re updated every time the user logs in. The PKCE extension provides a solution for securely doing the OAuth flow on a mobile app even when there is no pre-provisioned secret.

If you need to access an API from a mobile app, hopefully, it supports OAuth and PKCE! Thankfully most of the hard work of PKCE is handled by SDKs like AppAuth, so you don’t have to write all that code yourself. If you’re working with an API like Okta, then Okta’s SDKs do PKCE automatically, so you don’t have to worry about it. The JHipster Ionic blueprint uses Ionic AppAuth.

The previous sections showed you how to use Keycloak as your identity provider. If you’re deploying to production, you might not want to manage your users and authentication system. That’s where Okta and Auth0 can help!

Switch your identity provider to Okta

If you don’t have an Okta developer account, you can sign up for one or run okta register after installing the Okta CLI.

If you want to change your JHipster app to use Okta, the Okta CLI makes this as easy as okta apps create jhipster. This command creates a .okta.env file you can source to override the default Keycloak settings.

source .okta.env
./mvnw -Pprod

With Keycloak, you don’t need a separate OIDC app for Ionic. With Okta, you do. See JHipster’s documentation to learn how to create a native app for Ionic on Okta.

After you’ve changed the client ID in your Ionic app, run it using npm start. You’ll be prompted to log in using your Okta credentials at http://localhost:8100.

Switch your identity provider to Auth0

To switch your identity provider to Auth0, you first need an Auth0 account. Then, create a .auth0.env file and see JHipster’s Auth0 docs for how to populate it.

Next, configure a native app for Ionic on Auth0. Once you’re finished updating your Ionic app with a new client ID and audience, you should be able to run your backend and new frontend client using the following commands:

source .auth0.env
npm run backend:start
# open a new terminal
npm start

To see it in action on your mobile emulators, use the following commands:

npm run build

# iOS
npx cap run ios

# Android
npx cap run android

Original article sourced at: https://developer.okta.com

#angular 

How to Create Secure Ionic Apps with angular and JHipster
Coding  Life

Coding Life

1671420331

Spring Boot 3.0 | What’s new in 3.0 Release

In this Tutorial, we explore what we know about the Spring Boot 3 release, including potential features, improvements, changes to dependencies, and more

00:35 - intro
00:55 - Features Overview
04:40 - Project creation
08:2  - Java 17 Record
09:41 - Service & Controller
16:11 - Jakarta
17:8  - ProblemDetail
25:24 - Observability
34:41 - Spring Native
46:23 - HttpExchange

GralVM Setup : 
https://blogs.oracle.com/java/post/go-native-with-spring-boot-3-and-graalvm 

GitHub: 
https://github.com/Java-Techie-jt/spring-boot-3.0 

Subscribe: https://www.youtube.com/@Javatechie/featured 

#springboot 

Spring Boot 3.0 | What’s new in 3.0 Release
Enoch Barcenas

Enoch Barcenas

1671261386

How to use Spring Security 6 in Spring Boot 3

This tutorial will teach how to use Spring security 6 in Spring boot 3. Learn how to remove deprecated APIs, classes, and annotations in Spring security 6.

We will learn about formed-based authentication, basic authentication, in-memory authentication, method-level security, etc.

#springboot #springsecurity #security 

How to use Spring Security 6 in Spring Boot 3
Daisy Rees

Daisy Rees

1670659800

What’s New in Spring Framework 6 and Spring Boot 3.0

Spring Boot 3 - What’s new in Spring Framework 6 and Spring Boot 3.0

In this tutorial, you will learn about the newest features in Spring Framework 6 and Spring Boot 3. This includes the major themes such as

- Java 17 Baseline
- Jakarta EE 9/10
- Observability
- Native Images with GraalVM

You will also learn about some really great additions such as

- HTTP Interfaces
- Problem Details
- Spring Data 2022
- Spring Security 6

🔗Resources & Links mentioned in this video:

Github Repository: https://github.com/danvega/whats-new-spring-boot-3 

#springboot 

What’s New in Spring Framework 6 and Spring Boot 3.0