Build a Beautiful CRUD App with Spring Boot and Angular
Tags: java, springboot, angular, angularmaterial
Published: 2023-08-30 02:12:46

Angular is one of my favorite frameworks for building single-page applications (SPAs). As a Java developer, its separation of components, services, and pipes made a lot of sense to me. It's a web framework that allows you to declaratively describe your UI by creating small, reusable components. I believe it was a huge influencer in TypeScript's popularity. It's also backed by Google, which means it will likely be around for a long time.

I like to build CRUD (create, read, update, and delete) apps to understand frameworks. I think they show a lot of the base functionality you need when creating an app. Once you complete the basics of CRUD, most of the integration work is finished, and you can move on to implementing the necessary business logic.

In this tutorial, you'll learn how to build a secure CRUD app with Spring Boot and Angular. The final will result will use OAuth 2.0 authorization code flow and package the Angular app in the Spring Boot app for distribution as a single artifact. At the same time, I'll show you how to keep Angular's productive workflow when developing locally.

You'll need to install several tools to follow along with this tutorial.

**Prerequisites**:

- [Java 17](http://sdkman.io)
- [Node 18](https://nodejs.org/)
- [HTTPie](https://httpie.io/cli)
- [Auth0 CLI](https://github.com/auth0/auth0-cli#installation)
- [An Auth0 account](https://a0.to/blog_signup)

You can also watch [a demo of the completed example](https://youtu.be/0pnSVdVn_NM) in the screencast below:

{% embed https://youtu.be/0pnSVdVn_NM %}

## Configure and Run a Spring Boot and Angular App

I'm a frequent speaker at conferences and Java User Groups (JUGs) around the world. I've been a Java developer for 20+ years and ❤️ the Java community. I've found that speaking at JUGs is a great way to interact with the community and get raw feedback on presentations.

{% embed https://twitter.com/mraible/status/1623190784072720386 %}

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

I realize that taking 20 minutes to build this app can be cumbersome, so I've already built it in [@oktadev/auth0-spring-boot-angular-crud-example](https://github.com/oktadev/auth0-spring-boot-angular-crud-example). The project uses Spring Boot 3.2.0 and Angular 17. I hope this helps you clone, configure, and run! Seeing something running is such a joyful experience. 🤗

```shell
git clone https://github.com/oktadev/auth0-spring-boot-angular-crud-example jugtours
cd jugtours
```

Open a terminal window and run `auth0 login` to configure the Auth0 CLI to get an API key for your tenant. Then, run `auth0 apps create` to register this app with the appropriate URLs:

```shell
auth0 apps create \
  --name "Bootiful Angular" \
  --description "Spring Boot + Angular = ❤️" \
  --type regular \
  --callbacks http://localhost:8080/login/oauth2/code/okta,http://localhost:4200/login/oauth2/code/okta \
  --logout-urls http://localhost:8080,http://localhost:4200 \
  --reveal-secrets
```

Copy the outputted values from this command into a new `.okta.env` file:

```shell
export OKTA_OAUTH2_ISSUER=https://<your-auth0-domain>/
export OKTA_OAUTH2_CLIENT_ID=<your-client-id>
export OKTA_OAUTH2_CLIENT_SECRET=<your-client-secret>
```

If you're on Windows, use `set` instead of `export` to set these environment variables, and name the file `.okta.env.bat`:

```shell
set OKTA_OAUTH2_ISSUER=https://<your-auth0-domain>/
set OKTA_OAUTH2_CLIENT_ID=<your-client-id>
set OKTA_OAUTH2_CLIENT_SECRET=<your-client-secret>
```

Then, run `source .okta.env` (or `.okta.env.bat` on Windows) to set these environment variables in your current shell.

Finally, run `./mvnw` (or `mvnw` on Windows) to start the app.

```shell
source .okta.env # run .okta.env.bat on Windows
./mvnw -Pprod # use mvnw -Pprod on Windows
```

To view the app, you can then open `http://localhost:8080` in your favorite browser.

![JUG Tours homepage](https://images.ctfassets.net/23aumh6u8s0i/3ef88RDCni8WhXvzaS5R2f/72f357b0ca85749b6cd5f66007e2d6f5/home-with-login.png)

Click **Login**, and you'll be prompted to log in with Auth0. You'll also be asked for consent. This is because the app is requesting access to your profile and email address. Click **Accept** to continue.

![Auth0 consent](https://images.ctfassets.net/23aumh6u8s0i/5vzSqNivFOBVHSOViyHTuy/80646d5241c3ca5c64664d22b98b96fd/auth0-consent.png)

Once you're authenticated, you'll see a link to manage your JUG Tours.

![Manage JUG Tours](https://images.ctfassets.net/23aumh6u8s0i/7oO5PZBLl7t7NlppnFCE1A/6788ca86df0b9053e363a2c09ed1c8b4/manage-jugtours.png)

You should be able to add new groups and events, as well as edit and delete them.

![List of JUG Tours](https://images.ctfassets.net/23aumh6u8s0i/4ePxbb1YShcYB17puFc1ng/838cd725f8d2c6da5e0d37c737021c4f/jug-tours-list.png)

### Verify Cypress end-to-end tests pass

You can verify everything works by executing the Cypress tests that are included in the project. First, add environment variables for your credentials to the `.okta.env` (or `.okta.env.bat`) file you created earlier.

```shell
export CYPRESS_E2E_DOMAIN=<your-auth0-domain> # use the raw value, no https prefix
export CYPRESS_E2E_USERNAME=<your-email>
export CYPRESS_E2E_PASSWORD=<your-password>
```

Then, run `source .okta.env` (or `.okta.env.bat` on Windows) to set these environment variables.

Finally, run `npm run e2e` to start the app and run the Cypress tests.

```shell
cd app
npm run e2e
```

![Cypress tests running in Chrome](https://images.ctfassets.net/23aumh6u8s0i/55BrWm3WWrW69K0a2T3VUj/11efc7aa1aefdcee833261463bd5a17a/cypress-chrome.png)

Pretty slick, don't you think? 🤠

Read on if you'd like to see how I created this app!

## Create a Java REST API with Spring Boot

The easiest way to create a new Spring Boot app is to navigate to [start.spring.io](https://start.spring.io) and make the following selections:

* **Project:** `Maven Project`
* **Group:** `com.okta.developer`
* **Artifact:** `jugtours`
* **Dependencies**: `JPA`, `H2`, `Web`, `Validation`

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

You can also use [this link](https://start.spring.io/#!type=maven-project&language=java&platformVersion=3.2.0&packaging=jar&jvmVersion=17&groupId=com.okta.developer&artifactId=jugtours&name=jugtours&description=Track%20your%20JUG%20Tours!&packageName=com.okta.developer.jugtours&dependencies=data-jpa,h2,web,validation) or [HTTPie](https://httpie.io/) to create the project from the command line:

```shell
https start.spring.io/starter.tgz type==maven-project bootVersion==3.2.0 \
  dependencies==data-jpa,h2,web,validation \
  language==java platformVersion==17 \
  name==jugtours artifactId==jugtours \
  groupId==com.okta.developer packageName==com.okta.developer.jugtours \
  baseDir==jugtours | tar -xzvf -
```

### Add a JPA domain model

Open the jugtours project in your favorite IDE. Create a `src/main/java/com/okta/developer/jugtours/model` directory and a `Group.java` class in it.

```java
package com.okta.developer.jugtours.model;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;

import java.util.Set;

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

    @Id
    @GeneratedValue
    private Long id;
    @NotNull
    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;

    // JPA requires a default constructor
    public Group() {}
  
    public Group(String name) {
        this.name = name;
    }

    // getters and setters, equals, hashcode, and toString omitted for brevity
    // Why not Lombok? See https://twitter.com/mariofusco/status/1650439733212766208
    // Want Lombok anyway? See https://bit.ly/3HkaYMm and revert
}
```

Create an `Event.java` class in the same package.

```java
package com.okta.developer.jugtours.model;

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

import java.time.Instant;
import java.util.Set;

@Entity
public class Event {

    @Id
    @GeneratedValue
    private Long id;
    private Instant date;
    private String title;
    private String description;

    @ManyToMany
    private Set<User> attendees;

    public Event() {}
     
    public Event(Instant date, String title, String description) {
        this.date = date;
        this.title = title;
        this.description = description;
    }

    // you can generate the getters and setters using your IDE!
}
```

And a `User.java` class.

```java
package com.okta.developer.jugtours.model;

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

import java.util.Objects;

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

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

    public User() {}
  
    public User(String id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    // getters and setters omitted for brevity
}
```

Create a `GroupRepository.java` to manage the group entity.

```java
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.

```java
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("Utah JUG", "Dallas JUG", "Tampa JUG", "Nashville JUG", "Detroit JUG")
            .forEach(name -> repository.save(new Group(name)));

        Group jug = repository.findByName("Tampa JUG");
        Event e = new Event(Instant.parse("2024-04-24T18:00:00.000Z"),
            "What the Heck is OAuth?",
            "Learn how and where OAuth can benefit your applications.");
        jug.setEvents(Collections.singleton(e));
        repository.save(jug);

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

Start your app with `mvn spring-boot:run`, and you should see groups and events being created.

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

```java
package com.okta.developer.jugtours.web;

import com.okta.developer.jugtours.model.Group;
import com.okta.developer.jugtours.model.GroupRepository;
import jakarta.validation.Valid;
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 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 final 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();
    }
}
```

Restart the app, hit `http://localhost:8080/api/groups` with HTTPie, and you should see the list of groups.

    http :8080/api/groups

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

```shell
http POST :8080/api/group name='Toronto JUG' city='Toronto' country=CA
http :8080/api/group/6
http PUT :8080/api/group/6 id=6 name='Toronto JUG' address='16 York St'
http DELETE :8080/api/group/6
```

## Create an Angular App with the Angular CLI

The Angular CLI was a revolutionary tool when it was released in 2016. It's now the standard way to create new Angular projects and the easiest way to get started with Angular. Many web frameworks have adopted similar tools to improve their developer experience.

You don't have to install Angular CLI globally. The `npx` command can install and run it for you.

```shell
npx @angular/cli@17 new app --routing --style css --ssr false
```

Of course, you can use the tried-and-true `npm i -g @angular/cli` and `ng new app --routing --style css --ssr false` if you prefer. You can even remove the version number if you want to live on the edge.

After the app creation process completes, navigate into the `app` directory and install [Angular Material](https://material.angular.io/) to make the UI look beautiful, particularly on mobile devices.

```shell
cd app
ng add @angular/material
```

You'll be prompted to choose a theme, set up typography styles, and include animations. Select the defaults.

Modify `app/src/app/app.component.html` and move the CSS at the top to `app.component.css`.

### Call your Spring Boot API and display the results

Update `app.component.ts` to fetch the list of groups when it loads.

```typescript
import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { Group } from './model/group';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, HttpClientModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent implements OnInit {
  title = 'JUG Tours';
  loading = true;
  groups: Group[] = [];

  constructor(private http: HttpClient) {
  }

  ngOnInit() {
    this.loading = true;
    this.http.get<Group[]>('api/groups').subscribe((data: Group[]) => {
      this.groups = data;
      this.loading = false;
    });
  }
}
```

Before this compiles, you'll need to create a `app/src/app/model/group.ts` file with the following contents:

```typescript
export class Group {
  id: number | null;
  name: string;
  
  constructor(group: Partial<Group> = {}) {
    this.id = group?.id || null;
    this.name = group?.name || '';
  }
}
```

Then, modify the `app.component.html` file to display the list of groups.

```html
<main class="main">
  <div class="content">
    <div class="left-side">
      <h1>{{title}}</h1>
    </div>
    <div class="divider" role="separator" aria-label="Divider"></div>
    <div class="right-side">
      <div class="pill-group">
        @if (loading) {
        <p>Loading...</p>
        }
        @for (group of groups; track group) {
        <span class="pill">{{group.name}}</span>
        }
      </div>
    </div>
  </div>
</main>
<router-outlet></router-outlet>
```  

Create a file called `proxy.conf.js` in the `src` folder of your Angular project and use it to define your proxies:

```js
const PROXY_CONFIG = [
  {
    context: ['/api'],
    target: 'http://localhost:8080',
    secure: true,
    logLevel: 'debug'
  }
]

module.exports = PROXY_CONFIG;
```

Update `angular.json` and its `serve` command to use the proxy.

```json
"serve": {
  "builder": "@angular-devkit/build-angular:dev-server",
  "configurations": {
    "production": {
      "buildTarget": "app:build:production"
    },
    "development": {
      "buildTarget": "app:build:development",
      "proxyConfig": "src/proxy.conf.js"
    }
  },
  "defaultConfiguration": "development"
},
```

Stop your app with `Ctrl+C` and restart it with `npm start`. Now you should see a list of groups in your Angular app!

![JUG Tours list](https://images.ctfassets.net/23aumh6u8s0i/3GDjhsxMVp12WkAirk6xSV/8e9f18e9af3a6ced2008da573fdc5888/group-list.png)

### Build an Angular `GroupList` component

Angular is a component framework that allows you to separate concerns easily. You don't want to render everything in your main `AppComponent`, so create a new component to display the list of groups.

```shell
ng g c group-list
```

This command will create a new component in `src/app/group-list` with a TypeScript file, HTML template, CSS file, and a test file. Standalone components are created by default in Angular 17. They allow you to isolate components to be self-contained and, therefore, easier to distribute. 

Replace the code in `group-list.component.ts` with the following:

```typescript
import { Component } from '@angular/core';
import { Group } from '../model/group';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
import { MatIconModule } from '@angular/material/icon';
import { DatePipe } from '@angular/common';

@Component({
  selector: 'app-group-list',
  standalone: true,
  imports: [RouterLink, MatButtonModule, MatTableModule, MatIconModule, DatePipe, HttpClientModule],
  templateUrl: './group-list.component.html',
  styleUrl: './group-list.component.css'
})
export class GroupListComponent {
  title = 'Group List';
  loading = true;
  groups: Group[] = [];
  displayedColumns = ['id', 'name', 'events', 'actions'];
  feedback: any = {};

  constructor(private http: HttpClient) {
  }

  ngOnInit() {
    this.loading = true;
    this.http.get<Group[]>('api/groups').subscribe((data: Group[]) => {
      this.groups = data;
      this.loading = false;
      this.feedback = {};
    });
  }

  delete(group: Group): void {
    if (confirm(`Are you sure you want to delete ${group.name}?`)) {
      this.http.delete(`api/group/${group.id}`).subscribe({
        next: () => {
          this.feedback = {type: 'success', message: 'Delete was successful!'};
          setTimeout(() => {
            this.ngOnInit();
          }, 1000);
        },
        error: () => {
          this.feedback = {type: 'warning', message: 'Error deleting.'};
        }
      });
    }
  }

  protected readonly event = event;
}
```

Update its HTML template to use Angular Material's table component.

```html
<nav aria-label="breadcrumb">
  <ol class="breadcrumb">
    <li class="breadcrumb-item"><a routerLink="/">Home</a></li>
    <li class="breadcrumb-item active">Groups</li>
  </ol>
</nav>

<a [routerLink]="['/group/new']" mat-raised-button color="primary" style="float: right" id="add">Add Group</a>

<h2>{{title}}</h2>
@if (loading) {
<div>
  <p>Loading...</p>
</div>
} @else {
  @if (feedback.message) {
    <div class="alert alert-{{feedback.type}}">{{ feedback.message }}</div>
  }
  <table mat-table [dataSource]="groups">
    <ng-container matColumnDef="id">
      <mat-header-cell *matHeaderCellDef> ID </mat-header-cell>
      <mat-cell *matCellDef="let item"> {{ item.id }} </mat-cell>
    </ng-container>
    <ng-container matColumnDef="name">
      <mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
      <mat-cell *matCellDef="let item"> {{ item.name }} </mat-cell>
    </ng-container>
    <ng-container matColumnDef="events">
      <mat-header-cell *matHeaderCellDef> Events </mat-header-cell>
      <mat-cell *matCellDef="let item">
        @for (event of item.events; track event) {
          {{event.date | date }}: {{ event.title }}
          <br/>
        }
      </mat-cell>
    </ng-container>
    <ng-container matColumnDef="actions">
      <mat-header-cell *matHeaderCellDef> Actions </mat-header-cell>
      <mat-cell *matCellDef="let item">
        <a [routerLink]="['../group', item.id ]" mat-raised-button color="accent">Edit</a>&nbsp;
        <button (click)="delete(item)" mat-button color="warn"><mat-icon>delete</mat-icon></button>
      </mat-cell>
    </ng-container>
    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
  </table>
}
```

Create a `HomeComponent` to display a welcome message and a link to the groups page. This component will be the default route for the app.

```bash
ng g c home
```

Update `home.component.html` with the following:

```html
<a mat-button color="primary" href="/groups">Manage JUG Tour</a>
```

Add an import for `MatButtonModule` to `home.component.ts`:

```typescript
import { MatButtonModule } from '@angular/material/button';

@Component({
  selector: 'app-home',
  standalone: true,
  imports: [MatButtonModule],
  ...
})
```

Change `app.component.html` to  remove the list of groups above `<router-outlet>`:

```html
<div class="content" role="main">
  <router-outlet></router-outlet>
</div>
```

And remove the groups fetching logic from `app.component.ts`:

```typescript
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'JUG Tours';
}
```

Add a route for the `HomeComponent` and `GroupListComponent` to `app.routes.ts`:

```typescript
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { GroupListComponent } from './group-list/group-list.component';

export const routes: Routes = [
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  {
    path: 'home',
    component: HomeComponent
  },
  {
    path: 'groups',
    component: GroupListComponent
  }
];
```

Update the CSS in `styles.css` to have rules for the `breadcrumb` and `alert` classes:

```css
/* https://careydevelopment.us/blog/angular-how-to-add-breadcrumbs-to-your-ui */
ol.breadcrumb {
  padding: 0;
  list-style-type: none;
  margin: 5px 0 0 0;
}

.breadcrumb-item + .active {
  color: inherit;
  font-weight: 500;
}

.breadcrumb-item {
  color: #3F51B5;
  font-size: 1rem;
  text-decoration: underline;
  cursor: pointer;
}

.breadcrumb-item + .breadcrumb-item {
  padding-left: 0.5rem;
}

.breadcrumb-item + .breadcrumb-item::before {
  display: inline-block;
  padding-right: 0.5rem;
  color: rgb(108, 117, 125);
  content: "/";
}

ol.breadcrumb li {
  list-style-type: none;
}

ol.breadcrumb li {
  list-style-type: none;
  display: inline
}

.alert {
  padding: 0.75rem 1.25rem;
  margin-bottom: 1rem;
  border: 1px solid transparent;
}

.alert-success {
  color: #155724;
  background-color: #d4edda;
  border-color: #c3e6cb;
}

.alert-error {
  color: #721c24;
  background-color: #f8d7da;
  border-color: #f5c6cb;
}
```

Run `npm start` in your `app` directory to see how everything looks. Click on **Manage JUG Tour**, and you should see a list of the default groups.

![JUG Tours list](https://images.ctfassets.net/23aumh6u8s0i/3GDjhsxMVp12WkAirk6xSV/8e9f18e9af3a6ced2008da573fdc5888/group-list.png)

To squish the **Actions** column to the right, add the following to `group-list.component.css`:

```css
.mat-column-actions {
  flex: 0 0 120px;
}
```

Your Angular app should update itself as you make changes.

![Group list with squished actions column](https://images.ctfassets.net/23aumh6u8s0i/2w0zg2vU06JWNUKqYrM5Hk/3d0598e87e9181a14b62ef127cd8736f/group-list-actions.png)

It's great to see your Spring Boot API's data in your Angular app, but it's no fun if you can't modify it!

### Build an Angular `GroupEdit` component

Create a `group-edit` component and use Angular's `HttpClient` to fetch the group resource with the ID from the URL.

```shell
ng g c group-edit
```

Add a route for this component to `app.routes.ts`:

```typescript
import { GroupEditComponent } from './group-edit/group-edit.component';

export const routes: Routes = [
  ...
  {
    path: 'group/:id',
    component: GroupEditComponent
  }
];
```

Replace the code in `group-edit.component.ts` with the following:

```typescript
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { map, of, switchMap } from 'rxjs';
import { Group } from '../model/group';
import { Event } from '../model/event';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { MatInputModule } from '@angular/material/input';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatIconModule } from '@angular/material/icon';
import { MatNativeDateModule } from '@angular/material/core';
import { MatTooltipModule } from '@angular/material/tooltip';

@Component({
  selector: 'app-group-edit',
  standalone: true,
  imports: [
    FormsModule,
    HttpClientModule,
    MatInputModule,
    MatButtonModule,
    MatDatepickerModule,
    MatIconModule,
    MatNativeDateModule,
    MatTooltipModule,
    RouterLink
  ],
  templateUrl: './group-edit.component.html',
  styleUrl: './group-edit.component.css'
})
export class GroupEditComponent implements OnInit {
  group!: Group;
  feedback: any = {};

  constructor(private route: ActivatedRoute, private router: Router,
              private http: HttpClient) {
  }

  ngOnInit() {
    this.route.params.pipe(
      map(p => p['id']),
      switchMap(id => {
        if (id === 'new') {
          return of(new Group());
        }
        return this.http.get<Group>(`api/group/${id}`);
      })
    ).subscribe({
      next: group => {
        this.group = group;
        this.feedback = {};
      },
      error: () => {
        this.feedback = {type: 'warning', message: 'Error loading'};
      }
    });
  }

  save() {
    const id = this.group.id;
    const method = id ? 'put' : 'post';

    this.http[method](`/api/group${id ? '/' + id : ''}`, this.group).subscribe({
      next: () => {
        this.feedback = {type: 'success', message: 'Save was successful!'};
        setTimeout(async () => {
          await this.router.navigate(['/groups']);
        }, 1000);
      },
      error: () => {
        this.feedback = {type: 'error', message: 'Error saving'};
      }
    });
  }

  async cancel() {
    await this.router.navigate(['/groups']);
  }

  addEvent() {
    this.group.events.push(new Event());
  }

  removeEvent(index: number) {
    this.group.events.splice(index, 1);
  }
}
```

Create a `model/event.ts` file so this component will compile.

```typescript
export class Event {
  id: number | null;
  date: Date | null;
  title: string;

  constructor(event: Partial<Event> = {}) {
    this.id = event?.id || null;
    this.date = event?.date || null;
    this.title = event?.title || '';
  }
}
```

Update `model/group.ts` to include the `Event` class.

```typescript
import { Event } from './event';

export class Group {
  id: number | null;
  name: string;
  events: Event[];

  constructor(group: Partial<Group> = {}) {
    this.id = group?.id || null;
    this.name = group?.name || '';
    this.events = group?.events || [];
  }
}
```

The `GroupEditComponent` needs to render a form, so update `group-edit.component.html` with the following:

```html
<nav aria-label="breadcrumb">
  <ol class="breadcrumb">
    <li class="breadcrumb-item"><a routerLink="/">Home</a></li>
    <li class="breadcrumb-item"><a routerLink="/groups">Groups</a></li>
    <li class="breadcrumb-item active">Edit Group</li>
  </ol>
</nav>

<h2>Group Information</h2>
@if (feedback.message) {
<div class="alert alert-{{feedback.type}}">{{ feedback.message }}</div>
}
@if (group) {
<form #editForm="ngForm" (ngSubmit)="save()">
  @if (group.id) {
  <mat-form-field class="full-width">
    <mat-label>ID</mat-label>
    <input matInput [(ngModel)]="group.id" id="id" name="id" placeholder="ID" readonly>
  </mat-form-field>
  }
  <mat-form-field class="full-width">
    <mat-label>Name</mat-label>
    <input matInput [(ngModel)]="group.name" id="name" name="name" placeholder="Name" required>
  </mat-form-field>
  @if (group.events.length) {
  <h3>Events</h3>
  }
  @for (event of group.events; track event; let i = $index) {
  <div class="full-width">
    <mat-form-field style="width: 35%">
      <mat-label>Date</mat-label>
      <input matInput [matDatepicker]="picker"
             [(ngModel)]="group.events[i].date" name="group.events[{{i}}].date" placeholder="Date">
      <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
      <mat-datepicker #picker></mat-datepicker>
    </mat-form-field>
    <mat-form-field style="width: 65%">
      <mat-label>Title</mat-label>
      <input matInput [(ngModel)]="group.events[i].title" name="group.events[{{i}}].title" placeholder="Title">
    </mat-form-field>
    <button mat-icon-button (click)="removeEvent(i)" aria-label="Remove Event"
            style="float: right; margin: -70px -5px 0 0">
      <mat-icon>delete</mat-icon>
    </button>
  </div>
  }
  <div class="button-row" role="group">
    @if (group.id) {
    <button type="button" mat-mini-fab color="accent" (click)="addEvent()"
            aria-label="Add Event" matTooltip="Add Event"
            style="float: right; margin-top: -4px">
      <mat-icon>add</mat-icon>
    </button>
    }
    <button type="submit" mat-raised-button color="primary" [disabled]="!editForm.form.valid" id="save">Save</button>
    <button type="button" mat-button color="accent" (click)="cancel()" id="cancel">Cancel</button>
  </div>
</form>
}
```

If you look closely, you'll notice this component allows you to edit events for a group. This component is an excellent example of how to handle nested objects in Angular.

Update `group-edit.component.css` to make things look better on all devices:

```css
form, h2 {
  min-width: 150px;
  max-width: 700px;
  width: 100%;
  margin: 10px auto;
}

.alert {
  max-width: 660px;
  margin: 0 auto;
}

.full-width {
  width: 100%;
}
```

Now, with your Angular app running, you should be able to add and edit groups! Yaasss! 👏👏👏

![Edit a group and add events](https://images.ctfassets.net/23aumh6u8s0i/6kUt1VW6Nu8pK4gdFqABvr/71f5810d989ea420c713654091bfc4f1/group-edit.png)

To make the navbar at the top use Angular Material colors, add the following to the top of `app.component.html`:

```html
<mat-toolbar role="banner" color="primary">
  <svg id="angular-logo" height="35" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 960" fill="#fff">
    <g>
      <polygon points="562.6,109.8 804.1,629.5 829.2,233.1"/>
      <polygon points="624.9,655.9 334.3,655.9 297.2,745.8 479.6,849.8 662,745.8"/>
      <polygon points="384.1,539.3 575.2,539.3 479.6,307"/>
      <polygon points="396.6,109.8 130,233.1 155.1,629.5"/>
    </g>
  </svg>
  <span>{{ title }}</span>
  <div class="spacer"></div>
  <a aria-label="OktaDev on Twitter" target="_blank" rel="noopener" href="https://twitter.com/oktadev" title="Twitter">
    <svg id="twitter-logo" height="35" data-name="Logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
      <rect width="400" height="400" fill="none"/>
      <path
        d="M153.62,301.59c94.34,0,145.94-78.16,145.94-145.94,0-2.22,0-4.43-.15-6.63A104.36,104.36,0,0,0,325,122.47a102.38,102.38,0,0,1-29.46,8.07,51.47,51.47,0,0,0,22.55-28.37,102.79,102.79,0,0,1-32.57,12.45,51.34,51.34,0,0,0-87.41,46.78A145.62,145.62,0,0,1,92.4,107.81a51.33,51.33,0,0,0,15.88,68.47A50.91,50.91,0,0,1,85,169.86c0,.21,0,.43,0,.65a51.31,51.31,0,0,0,41.15,50.28,51.21,51.21,0,0,1-23.16.88,51.35,51.35,0,0,0,47.92,35.62,102.92,102.92,0,0,1-63.7,22A104.41,104.41,0,0,1,75,278.55a145.21,145.21,0,0,0,78.62,23"
        fill="#fff"/>
    </svg>
  </a>
  <a aria-label="OktaDev on YouTube" target="_blank" rel="noopener" href="https://youtube.com/oktadev" title="YouTube">
    <svg id="youtube-logo" height="35" data-name="Logo" xmlns="http://www.w3.org/2000/svg"
         viewBox="0 0 24 24" fill="#fff">
      <path d="M0 0h24v24H0V0z" fill="none"/>
      <path
        d="M21.58 7.19c-.23-.86-.91-1.54-1.77-1.77C18.25 5 12 5 12 5s-6.25 0-7.81.42c-.86.23-1.54.91-1.77 1.77C2 8.75 2 12 2 12s0 3.25.42 4.81c.23.86.91 1.54 1.77 1.77C5.75 19 12 19 12 19s6.25 0 7.81-.42c.86-.23 1.54-.91 1.77-1.77C22 15.25 22 12 22 12s0-3.25-.42-4.81zM10 15V9l5.2 3-5.2 3z"/>
    </svg>
  </a>
</mat-toolbar>
```

Since this is not a standalone component, you must import `MatToolbarModule` in `app.component.ts`.

```typescript
import { MatToolbarModule } from '@angular/material/toolbar';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, MatToolbarModule],
  ...
})
```

Make some adjustments in `app.component.css` to make the toolbar look nicer.

1. Change the margin for `.social-links` to `.5rem` and remove the rules for `.social-links path` and `.social-links a:hover` below it.
2. Add a `.spacer` rule with `flex: 1 1 auto` in its properties.
3. Change the `.content` rule to be as follows:
    
```css
.content {
  display: flex;
  margin: 10px auto;
  padding: 0 16px;
  max-width: 960px;
  flex-direction: column;
  align-items: stretch;
}
```

Now the app fills the screen more, and the toolbar has matching colors.

![Toolbar colors match](https://images.ctfassets.net/23aumh6u8s0i/1P4MlwU1tzq2WPeOLKelLS/04e07d2b4db99dfe9096f817d785d3b7/toolbar-colors.png)

## Secure Spring Boot with OpenID Connect and OAuth

I love building simple CRUD apps to learn a new tech stack, but I think it's even cooler to build a _secure_ one. So let's do that!

Spring Security added support for OpenID Connect (OIDC) in version 5.0, circa 2017. This is awesome because it means you can use Spring Security to secure your app with a third-party identity provider (IdP) like Auth0. This is a much better option than trying to build your own authentication system and store user credentials.

Add the Okta Spring Boot starter to do OIDC authentication in your `pom.xml`. This will also add Spring Security to your app.

```xml
<dependency>
    <groupId>com.okta.spring</groupId>
    <artifactId>okta-spring-boot-starter</artifactId>
    <version>3.0.6</version>
</dependency>
```

Install the [Auth0 CLI](https://github.com/auth0/auth0-cli) (if you haven't already) and run `auth0 login` in a shell.

Next, run `auth0 apps create` to register a new OIDC app with appropriate callbacks:

```shell
auth0 apps create \
  --name "Bootiful Angular" \
  --description "Spring Boot + Angular = ❤️" \
  --type regular \
  --callbacks http://localhost:8080/login/oauth2/code/okta,http://localhost:4200/login/oauth2/code/okta \
  --logout-urls http://localhost:8080,http://localhost:4200 \
  --reveal-secrets
```

Copy the returned values from this command into an `.okta.env` file:

```shell
export OKTA_OAUTH2_ISSUER=https://<your-auth0-domain>/
export OKTA_OAUTH2_CLIENT_ID=<your-client-id>
export OKTA_OAUTH2_CLIENT_SECRET=<your-client-secret>
```

If you're on Windows, use `set` instead of `export` to set these environment variables and name the file `.okta.env.bat`:

```shell
set OKTA_OAUTH2_ISSUER=https://<your-auth0-domain>/
set OKTA_OAUTH2_CLIENT_ID=<your-client-id>
set OKTA_OAUTH2_CLIENT_SECRET=<your-client-secret>
```

Add `*.env` to your `.gitignore` file so you don't accidentally expose your client secret.

Then, run `source .okta.env` (or `.okta.env.bat` on Windows) to set these environment variables in your current shell.

Finally, run `./mvnw` (or `mvnw` on Windows) to start the app.

```shell
source .okta.env
./mvnw spring-boot:run
```

You can then open `http://localhost:8080` in your favorite browser. You'll be redirected to authenticate and returned afterward. You'll see a 404 error from Spring Boot since you have nothing mapped to the default `/` route.

![Spring Boot 404](https://images.ctfassets.net/23aumh6u8s0i/3Au5kGN8i9w4sar9laa5sq/087d6df3bd92511c6e11c1c7f9a5772b/404.png)

### Configure Spring Security for maximum protection

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

```java
package com.okta.developer.jugtours.config;

import com.okta.developer.jugtours.web.CookieCsrfFilter;
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 static org.springframework.security.config.Customizer.withDefaults;

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((authz) -> authz
                .requestMatchers("/", "/index.html", "*.ico", "*.css", "*.js", "/api/user").permitAll()
                .anyRequest().authenticated())
            .oauth2Login(withDefaults())
            .oauth2ResourceServer((oauth2) -> oauth2.jwt(withDefaults()))
            .csrf((csrf) -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()))
            .addFilterAfter(new CookieCsrfFilter(), BasicAuthenticationFilter.class);

        return http.build();
    }
}
```

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. Since Spring Security 3.1, it's deprecated, and you should use `authorizeHttpRequests()`. The `authorizeRequests()` lambda is 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 Angular app has.

The `requestMatchers` line defines the URLs allowed for anonymous users. You will soon configure things so your Spring Boot app serves up your Angular app, hence the reason for allowing `/`, `/index.html`, and web files. You might also notice an exposed `/api/user` path.

Configuring CSRF (cross-site request forgery) protection with `CookieCsrfTokenRepository.withHttpOnlyFalse()` means that the `XSRF-TOKEN` cookie won't be marked HTTP-only, so Angular 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. To learn more, you can read [this Stack Overflow answer](https://stackoverflow.com/a/74521360/65681). 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.

```java
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 an 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. Angular will use this API to 1) find out if a user is authenticated and 2) perform global logout.

```java
package com.okta.developer.jugtours.web;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
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.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.text.MessageFormat;

import static java.util.Map.of;

@RestController
public class UserController {
    private final 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) {
        // send logout URL to client so they can initiate logout
        var issuerUri = registration.getProviderDetails().getIssuerUri();
        var originUrl = request.getHeader(HttpHeaders.ORIGIN);
        Object[] params = {issuerUri, registration.getClientId(), originUrl};
        // Yes! We @ Auth0 should have an end_session_endpoint in our OIDC metadata.
        // It's not included at the time of this writing, but will be coming soon!
        var logoutUrl = MessageFormat.format("{0}v2/logout?client_id={1}&returnTo={2}", params);
        request.getSession().invalidate();
        return ResponseEntity.ok().body(of("logoutUrl", logoutUrl));
    }
}
```

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`.

```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`.

```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.

```java
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 final GroupRepository groupRepository;
    private final 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 highlight the changes, please review the `groups()` and `createGroup()` methods above. I think it's pretty slick that Spring JPA will create the `findAllByUserId()` method/query for you.

### Update Angular to handle CSRF and be identity-aware

I like Angular because it's a secure-first framework. It has built-in support for CSRF, and it's easy to make it identity-aware. Let's do both!

Angular's `HttpClient` supports the client-side half of the CSRF protection. It'll read the cookie sent by Spring Boot and return it in an `X-XSRF-TOKEN` header. You can read more about this at [Angular's Security docs](https://angular.io/guide/http#security-xsrf-protection).

### Update your Angular app's authentication mechanism

Create a new `AuthService` class to communicate with your Spring Boot API for authentication information. Add the following code to a new file at `app/src/app/auth.service.ts`.

```typescript
import { Injectable } from '@angular/core';
import { Location } from '@angular/common';
import { BehaviorSubject, lastValueFrom, Observable } from 'rxjs';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { map } from 'rxjs/operators';
import { User } from './model/user';

const headers = new HttpHeaders().set('Accept', 'application/json');

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  $authenticationState = new BehaviorSubject<boolean>(false);

  constructor(private http: HttpClient, private location: Location) {
  }

  getUser(): Observable<User> {
    return this.http.get<User>('/api/user', {headers}, )
      .pipe(map((response: User) => {
          if (response !== null) {
            this.$authenticationState.next(true);
          }
          return response;
        })
      );
  }

  async isAuthenticated(): Promise<boolean> {
    const user = await lastValueFrom(this.getUser());
    return user !== null;
  }

  login(): void {
    location.href = `${location.origin}${this.location.prepareExternalUrl('oauth2/authorization/okta')}`;
  }

  logout(): void {
    this.http.post('/api/logout', {}, { withCredentials: true }).subscribe((response: any) => {
      location.href = response.logoutUrl;
    });
  }
}
```

Add the referenced `User` class to `app/src/app/model/user.ts`.

```typescript
export class User {
  email!: number;
  name!: string;
}
```

`AuthService` depends on `HttpClient`, so you must update `app.config.ts` to import and use `provideHttpClient`.

```typescript
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
   providers: [provideRouter(routes), provideAnimations(), provideHttpClient()]
};
```

Modify `home.component.ts` to use `AuthService` to see if the user is logged in.

```typescript
import { Component, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterLink } from '@angular/router';
import { User } from '../model/user';
import { AuthService } from '../auth.service';

@Component({
  selector: 'app-home',
  standalone: true,
  imports: [MatButtonModule, RouterLink],
  templateUrl: './home.component.html',
  styleUrl: './home.component.css'
})
export class HomeComponent implements OnInit {
  isAuthenticated!: boolean;
  user!: User;

  constructor(public auth: AuthService) {
  }

  async ngOnInit() {
    this.isAuthenticated = await this.auth.isAuthenticated();
    this.auth.getUser().subscribe(data => this.user = data);
  }
}
```

Modify `home.component.html` to show the Login button if the user is not logged in. Otherwise, show a Logout button.

```html
@if (user) {
  <h2>Welcome, {{user.name}}!</h2>
  <a mat-button color="primary" routerLink="/groups">Manage JUG Tour</a>
  <br/><br/>
  <button mat-raised-button color="primary" (click)="auth.logout()" id="logout">Logout</button>
} @else {
  <p>Please log in to manage your JUG Tour.</p>
  <button mat-raised-button color="primary" (click)="auth.login()" id="login">Login</button>
}
```

Update `app/src/proxy.conf.js` to have additional proxy paths for `/oauth2` and `/login`.

```javascript
const PROXY_CONFIG = [
  {
    context: ['/api', '/oauth2', '/login'],
    ...
  }
]
```

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

![Angular app with Login](https://images.ctfassets.net/23aumh6u8s0i/2pyY9qP9U7iSWEDWdLSJVU/c48745a100c87e24db1e866fafd91a98/login.png)

![Angular app with Logout](https://images.ctfassets.net/23aumh6u8s0i/5OSFK49AIYdamCFyZVQuJ9/28a53b1d59d42bc90a5d2e415d64ad79/logout.png)

## Configure Maven to Package Angular with Spring Boot

To build and package your Angular app with Maven, you can use the [frontend-maven-plugin](https://github.com/eirslett/frontend-maven-plugin) and Maven's profiles to activate it. Add properties for versions and a `<profiles>` section to your `pom.xml`.

```xml
<properties>
    ...
    <frontend-maven-plugin.version>1.15.0</frontend-maven-plugin.version>
    <node.version>v18.18.2</node.version>
    <npm.version>9.8.1</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/dist/app/browser</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>npm</goal>
                            </goals>
                            <phase>test</phase>
                            <configuration>
                                <arguments>test -- --watch=false</arguments>
                            </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`:

```properties
spring.profiles.active=@spring.profiles.active@
```

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

If you start at the root, everything will work fine since Angular 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 Angular app.

```java
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.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class SpaWebFilter extends OncePerRequestFilter {

    /**
     * Forwards any unmapped paths (except those containing a period) to the client {@code index.html}.
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        String path = request.getRequestURI();
        if (!path.startsWith("/api") &&
            !path.startsWith("/login") &&
            !path.startsWith("/oauth2") &&
            !path.contains(".") &&
            path.matches("/(.*)")) {
            request.getRequestDispatcher("/index.html").forward(request, response);
            return;
        }

        filterChain.doFilter(request, response);
    }
}
```

And add to your `SecurityConfiguration.java` class:

```java
.addFilterAfter(new SpaWebFilter(), BasicAuthenticationFilter.class);
```

Now, if you restart, login, and navigate to `/groups`, refreshing will work as expected. 🤩

## Verify Everything Works with Cypress

In this section, you'll learn how to integrate Cypress into this project to support end-to-end tests. Open a terminal, navigate to the `app` directory, and add the [Cypress Angular Schematic](https://www.npmjs.com/package/@cypress/schematic):

    ng add @cypress/schematic

Select the default answers when prompted. Then, update `app/cypress/support/commands.ts` to add a `login(username, password)` method: 

```typescript
/* eslint-disable @typescript-eslint/no-namespace */
/* eslint-disable @typescript-eslint/no-use-before-define */
// eslint-disable-next-line spaced-comment
/// <reference types="cypress" />

Cypress.Commands.add('login', (username: string, password: string) => {
  Cypress.log({
    message: [`🔐 Authenticating: ${username}`],
    autoEnd: false,
  })
  cy.origin(Cypress.env('E2E_DOMAIN'), {args: {username, password}},
    ({username, password}) => {
      cy.get('input[name=username]').type(username);
      cy.get('input[name=password]').type(`${password}{enter}`, {log: false});
    }
  );
});

declare global {
  namespace Cypress {
    interface Chainable {
      login(username: string, password: string): Cypress.Chainable;
    }
  }
}

// Convert this to a module instead of script (allows import/export)
export {};
```

Update `app/cypress/support/e2e.ts` to log in before each test and log out after.

```typescript
import './commands';

beforeEach(() => {
  if (Cypress.env('E2E_USERNAME') === undefined) {
    console.error('E2E_USERNAME is not defined');
    alert('E2E_USERNAME is not defined');
    return;
  }
  cy.visit('/')
  cy.get('#login').click()
  cy.login(
    Cypress.env('E2E_USERNAME'),
    Cypress.env('E2E_PASSWORD')
  )
})

afterEach(() => {
  cy.visit('/')
  cy.get('#logout').click()
})
```

Rename `app/cypress/e2e/spec.cy.ts` to `home.cy.ts` and use it to verify that the home page loads.

```typescript
describe('Home', () => {
  beforeEach(() => {
    cy.visit('/')
  });

  it('Visits the initial app page', () => {
    cy.contains('JUG Tours')
    cy.contains('Logout')
  })
})
```

Create a `groups.cy.ts` in the same directory to test CRUD on groups.

```typescript
describe('Groups', () => {

  beforeEach(() => {
    cy.visit('/groups')
  });

  it('add button should exist', () => {
    cy.get('#add').should('exist');
  });

  it('should add a new group', () => {
    cy.get('#add').click();
    cy.get('#name').type('Test Group');
    cy.get('#save').click();
    cy.get('.alert-success').should('exist');
  });

  it('should edit a group', () => {
    cy.get('a').last().click();
    cy.get('#name').should('have.value', 'Test Group');
    cy.get('#cancel').click();
  });

  it('should delete a group', () => {
    cy.get('button').last().click();
    cy.on('window:confirm', () => true);
    cy.get('.alert-success').should('exist');
  });
});
```

Add environment variables with your credentials to the `.okta.env` (or `.okta.env.bat`) file you created earlier.

```shell
export CYPRESS_E2E_DOMAIN=<your-auth0-domain> # use the raw value, no https prefix
export CYPRESS_E2E_USERNAME=<your-email>
export CYPRESS_E2E_PASSWORD=<your-password>
```

Then, run `source .okta.env` (or `.okta.env.bat` on Windows) to set these environment variables and start the app.

```shell
mvn spring-boot:run -Pprod
```

In another terminal window, run the Cypress tests with Electron.

```shell
source .okta.env
cd app
npx cypress run --browser electron --config baseUrl=http://localhost:8080
```

![Cypress tests running in Electron](https://images.ctfassets.net/23aumh6u8s0i/5jaNNpmdJ94pPOZqkzGCf1/6570938d090ae653ea0eae947304cfdb/cypress-run.png)

### Fix Unit Tests

If you run `npm test`, you'll see several failures. That's because the components have dependencies that aren't imported in the tests. Update `home.component.spec.ts` to import `HttpClientTestingModule`:

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { HomeComponent } from './home.component';

describe('HomeComponent', () => {
  let component: HomeComponent;
  let fixture: ComponentFixture<HomeComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [HomeComponent, HttpClientTestingModule],
    }).compileComponents();
    
    fixture = TestBed.createComponent(HomeComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
```

In `app.component.spec.ts`, import `MatToolBarModule` and look for `JUG Tours` in the page.

```typescript
import { TestBed } from '@angular/core/testing';
import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [RouterTestingModule, MatToolbarModule],
    }).compileComponents();
  });

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });

  it(`should have the 'JUG Tours' title`, () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app.title).toEqual('JUG Tours');
  });

  it('should render title', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.nativeElement as HTMLElement;
    expect(compiled.querySelector('mat-toolbar > span')?.textContent).toContain('JUG Tours');
  });
});
```

Then, update both group component tests to import `HttpClientTestingModule` and `RouterTestingModule`.

```typescript
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RouterTestingModule } from '@angular/router/testing';

describe('...', () => {
  ...

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [..., HttpClientTestingModule, RouterTestingModule]
    });
    ...
  });

  ...
});
```

Now, `npm test` should pass.

If you run `mvn test` without setting environment variables, your Java tests will fail too. To fix this, add a `src/test/java/com/okta/developer/jugtours/TestSecurityConfiguration.java` class to mock the OAuth provider.

```java
package com.okta.developer.jugtours;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jwt.JwtDecoder;

import java.util.HashMap;
import java.util.Map;

import static org.mockito.Mockito.mock;

/**
 * This class allows you to run unit and integration tests without an IdP.
 */
@TestConfiguration
public class TestSecurityConfiguration {

    @Bean
    ClientRegistration clientRegistration() {
        return clientRegistrationBuilder().build();
    }

    @Bean
    ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) {
        return new InMemoryClientRegistrationRepository(clientRegistration);
    }

    private ClientRegistration.Builder clientRegistrationBuilder() {
        Map<String, Object> metadata = new HashMap<>();
        metadata.put("end_session_endpoint", "https://example.org/logout");

        return ClientRegistration.withRegistrationId("oidc")
            .issuerUri("{baseUrl}")
            .redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .scope("read:user")
            .authorizationUri("https://example.org/login/oauth/authorize")
            .tokenUri("https://example.org/login/oauth/access_token")
            .jwkSetUri("https://example.org/oauth/jwk")
            .userInfoUri("https://api.example.org/user")
            .providerConfigurationMetadata(metadata)
            .userNameAttributeName("id")
            .clientName("Client Name")
            .clientId("client-id")
            .clientSecret("client-secret");
    }

    @Bean
    JwtDecoder jwtDecoder() {
        return mock(JwtDecoder.class);
    }

    @Bean
    OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
        return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
    }
}
```

Then, update `JugToursApplicationTests.java` in the same directory to use the new configuration.

```java
@SpringBootTest(classes = {JugtoursApplication.class, TestSecurityConfiguration.class})
```

Run `mvn test` again, and your tests will pass. 🎉

## Use GitHub Actions to Build and Test Your App

Add a GitHub workflow at `.github/workflows/main.yml` to prove that your tests run in CI.

```yaml
name: JUG Tours CI

on: [push, pull_request]

jobs:
  build:
    name: Build and Test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Set up Java 17
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: 17
          cache: 'maven'
      - name: Run tests
        run: xvfb-run mvn verify -ntp -Pprod
      - name: Run e2e tests
        uses: cypress-io/github-action@v6
        with:
          browser: chrome
          start: mvn spring-boot:run -Pprod -ntp -f ../pom.xml
          install: false
          wait-on: http://localhost:8080
          wait-on-timeout: 120
          config: baseUrl=http://localhost:8080
          working-directory: app
        env:
          OKTA_OAUTH2_ISSUER: ${{ secrets.OKTA_OAUTH2_ISSUER }}
          OKTA_OAUTH2_CLIENT_ID: ${{ secrets.OKTA_OAUTH2_CLIENT_ID }}
          OKTA_OAUTH2_CLIENT_SECRET: ${{ secrets.OKTA_OAUTH2_CLIENT_SECRET }}
          CYPRESS_E2E_DOMAIN: ${{ secrets.CYPRESS_E2E_DOMAIN }}
          CYPRESS_E2E_USERNAME: ${{ secrets.CYPRESS_E2E_USERNAME }}
          CYPRESS_E2E_PASSWORD: ${{ secrets.CYPRESS_E2E_PASSWORD }}
      - name: Upload screenshots
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: cypress-screenshots
          path: app/cypress/screenshots
```

You can see this workflow in action [on GitHub](https://github.com/oktadev/auth0-spring-boot-angular-crud-example/actions/workflows/main.yml), or you can try it yourself by creating a new GitHub repo and pushing your code to it.

Add environment variables for the above secrets to your GitHub repository at **Settings** > **Secrets and variables** > **Actions** > **New Repository Secret**.

![GitHub repository secrets](https://images.ctfassets.net/23aumh6u8s0i/3FGyU7DuYXgvbLMR12L7gW/a9a641e177dd1e815bdf6375eab79473/repository-secrets.png)

Push your changes to GitHub and watch the CI workflow run. ✅

## Build Something Fabulous with Spring Boot and Angular!

I hope this post has helped you learn how to build secure Angular and Spring Boot apps. Using OpenID Connect is a recommended practice for authenticating full-stack apps like this one, and Auth0 makes it easy to do. Adding CSRF protection and packaging your Spring Boot + Angular app as a single artifact is super cool too!

We've written some other fun Spring Boot, Angular, and JHipster tutorials. Check them out!

- [Build a Simple CRUD App with Spring Boot and Vue.js](https://auth0.com/blog/build-crud-spring-and-vue/)
- [Use React and Spring Boot to Build a Simple CRUD App](https://auth0.com/blog/simple-crud-react-and-spring-boot/)
- [Add OpenID Connect to Angular Apps Quickly](https://auth0.com/blog/add-oidc-to-angular-apps-quickly/)
- [Full Stack Java with React, Spring Boot, and JHipster](https://auth0.com/blog/full-stack-java-with-react-spring-boot-and-jhipster/)

I've also written a couple of InfoQ mini-books that you might find useful:

- [The JHipster Mini-Book](https://www.infoq.com/minibooks/jhipster-mini-book/): Shows how I built [21-Points Health](https://www.21-points.com) with JHipster (Angular, Spring Boot, Bootstrap, and more). It includes a chapter on microservices with Spring Boot, React, and Auth0.
- [The Angular Mini-Book](https://www.infoq.com/minibooks/angular-mini-book/): A practical guide to Angular, Bootstrap, and Spring Boot. It uses Kotlin and Gradle, recommended security practices, and contains several cloud deployment guides.

If you have any questions, please leave a comment below. If you want to see the completed code for this tutorial, check out its [GitHub repo](https://github.com/oktadev/auth0-spring-boot-angular-crud-example). Follow us on [Twitter](https://twitter.com/oktadev) and [YouTube](https://youtube.com/oktadev) for more content like this.