Skip to content

Commit

Permalink
Merge pull request #32 from JewelleryManagement/feature-19-user-authe…
Browse files Browse the repository at this point in the history
…ntication

Implemented user authentication
  • Loading branch information
VladoKat authored Oct 5, 2023
2 parents e02d772 + 6a7871d commit 687c066
Show file tree
Hide file tree
Showing 40 changed files with 1,298 additions and 92 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ build/

# Gradle files
.gradle/
build/

# Spring Boot properties
.env
/src/main/resources/data.sql

pom.xml.tag
pom.xml.releaseBackup
Expand Down
69 changes: 58 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,69 @@
# Jewellery Management Service

A jewellery store management project
This is the backend service for a jewellery management administration system.

## Prerequisites
## Setup and Running the Project

### Prerequisites

What things you need to install the software and how to install them. For example:
You would need the following tools installed before running the project locally:

- Java 17
- Maven
- IntelliJ IDEA (or any preferred IDE)
- Docker

## Setup and Running the Project
[Click here to open instructions on how to create and run the docker container with postgres database](https://docs.google.com/document/d/12QSq2K_E1DIsF0a99rClwVmwdu7eLKnVagnG0i9JjBI/edit?usp=drive_link)
### Running the project

### Clone the Repository
1. Create .env file in the root folder with database credentials:
```
JMS_DATABASE_NAME=jewellery-management
JMS_DATABASE_USER=admin
JMS_DATABASE_PASSWORD=DB-p@s5w0rD
```
2. Start DB
- run `docker-compose up` in a terminal in the root folder
- This command will start a postgreSQL DB in a docker container with the properties we've entered in the .env file
3. Setup root user for authenticated access
- Create data.sql file in src/main/resources with the following content:
```
INSERT INTO users (id, name, email, password, role)
VALUES ('88596531-7f0f-407d-b502-31833b8c8e8d', 'root', 'root@gmail.com', '$2a$12$fGuoN79WFwHPUmirHOlxIO9kdmMTBrlNGKob0ay4muxXNDePg38ri', 'ADMIN')
ON CONFLICT (email) DO UPDATE
SET name = 'root', email = 'root@gmail.com', password = '$2a$12$fGuoN79WFwHPUmirHOlxIO9kdmMTBrlNGKob0ay4muxXNDePg38ri', role = 'ADMIN';
```
The password field is the bcrypt encoded value of `p@s5W07d`. You can either use this or choose your own
secure
password and put it through a [b-crypt generator](https://bcrypt-generator.com)
4. Setup IntelliJ environment variables
- Run -> Edit Configurations, then under Environment Variables, you should add the following:
```
JMS_DATABASE_NAME=jewellery-management;JMS_DATABASE_USER=admin;JMS_DATABASE_PASSWORD=DB-p@s5w0rD;SECRET_KEY=9dDDE3/Z7EdcCqA35PbruWDfEt0Dxk5cbPGaaudhJ5o=
```
The first 3 parameters are responsible for database connection and should match the ones we set up in
step 1. The
last one is a key for JWT token encoding. You can choose to use a different one.

```bash
git clone <https://github.com/JewelleryManagement/jewellery-management-service.git>
```
1. Follow instructions in file for creating the database docker container
2. Run the project from JewelleryInventoryApplication.java
5. Start the app
- run `mvn clean install` in a terminal to get all the needed dependencies and to build the project
- Run -> Run -> choose the configuration you set up in step 4
- The app should be running on localhost:8080
6. Interact with the app
- Send POST to `localhost:8080/login` with JSON body with payload:
```json
{
"email": "root@gmail.com",
"password": "p@s5W07d" // or the password you have chosen yourself
}
```
- The response will contain a token. You'd need to include this token in the Authorization
header in every
other request you'd want to send to the service.
- Authorization header: `Authorization: Bearer <token>`
- Access any endpoint of the service
- Example of a `GET /resources` request using curl:
```bash
curl --location 'localhost:8080/resources/quantity/d3515db3-d8a0-4807-bfae-40d53da0405a' \
--header 'Authorization: Bearer
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyb290QGdtYWlsLmNvbSIsImlhdCI6MTY5NjQ5NjMzMywiZXhwIjoxNjk2NTgyNzMzfQ.WqZMlAvLWkPbqepGrdpwfQY1dG39Jr_69npIWJQb_3U'
```
36 changes: 33 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,44 @@
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
<testcontainers.version>1.18.3</testcontainers.version>
<lombok.version>1.18.22</lombok.version>
<lombok-mapstruct-binding>0.2.0</lombok-mapstruct-binding>
<lombok-mapstruct-binding-version>0.2.0</lombok-mapstruct-binding-version>
<jjwt-version>0.11.5</jjwt-version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt-version}</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt-version}</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt-version}</version>
</dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
Expand All @@ -51,7 +81,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding}</version>
<version>${lombok-mapstruct-binding-version}</version>
</dependency>

<!-- snakeyaml 2.0 dependency is only added to fix vulnerability coming from
Expand Down Expand Up @@ -158,7 +188,7 @@
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding}</version>
<version>${lombok-mapstruct-binding-version}</version>
</path>
</annotationProcessorPaths>
</configuration>
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/jewellery/inventory/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package jewellery.inventory.controller;

import jakarta.validation.Valid;
import jewellery.inventory.dto.request.AuthenticationRequestDto;
import jewellery.inventory.dto.response.UserAuthDetailsDto;
import jewellery.inventory.service.security.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/login")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;

@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public UserAuthDetailsDto login(@Valid @RequestBody AuthenticationRequestDto authRequest) {
return authService.login(authRequest);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package jewellery.inventory.controller;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@CrossOrigin(origins = "${cors.origins}")
public class HomeController {

@GetMapping("/home")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

@RestController
@RequestMapping("/resources")
@CrossOrigin(origins = "${cors.origins}")
@RequiredArgsConstructor
public class ResourceController {
private final ResourceService resourceService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import jewellery.inventory.service.ResourceInUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -20,7 +19,6 @@

@RestController
@RequestMapping("/resources/availability")
@CrossOrigin(origins = "${cors.origins}")
@RequiredArgsConstructor
public class ResourceInUserController {
private final ResourceInUserService resourceAvailabilityService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import jewellery.inventory.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -21,7 +20,6 @@

@RestController
@RequestMapping("/users")
@CrossOrigin(origins = "${cors.origins}")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package jewellery.inventory.dto.request;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationRequestDto {
@NotBlank(message = "Email must not be blank, empty or null")
private String email;

@NotBlank(message = "Password must not be blank, empty or null")
private String password;
}
28 changes: 17 additions & 11 deletions src/main/java/jewellery/inventory/dto/request/UserRequestDto.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
package jewellery.inventory.dto.request;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class UserRequestDto {
private static final String NAME_PATTERN_REGEX = "^(?!.*__)[A-Za-z0-9_]*$";
private static final String EMAIL_PATTERN_REGEX =
"^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$";
private static final String NAME_SIZE_VALIDATION_MSG = "Size must be between 3 and 64";
private static final String NAME_PATTERN_VALIDATION_MSG =
"Name must only contain alphanumeric characters and underscores, and no consecutive underscores";
private static final String EMAIL_VALIDATION_MSG = "Email must be valid";
private static final String PWD_PATTERN_VALIDATION_MSG =
"Password must contain at least one digit, one lowercase letter, one uppercase letter, one special character, and be at least 8 characters long";

@NotEmpty
@Size(min = 3, max = 64, message = NAME_SIZE_VALIDATION_MSG)
@Pattern(regexp = NAME_PATTERN_REGEX, message = NAME_PATTERN_VALIDATION_MSG)
@NotBlank(message = "Name must not be blank, empty or null")
@Size(min = 3, max = 64, message = "Name size must be between 3 and 64")
@Pattern(regexp = "^(?!.*__)[A-Za-z0-9_]*$", message = NAME_PATTERN_VALIDATION_MSG)
private String name;

@NotEmpty
@Pattern(regexp = EMAIL_PATTERN_REGEX, message = EMAIL_VALIDATION_MSG)
@NotBlank(message = "Email must not be blank, empty or null")
@Pattern(
regexp = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$",
message = "Email must be valid")
private String email;

@NotBlank(message = "Password must not be blank, empty or null")
@Size(min = 8, message = "Size must be at least 8 characters")
@Pattern(
regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$",
message = PWD_PATTERN_VALIDATION_MSG)
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package jewellery.inventory.dto.response;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class UserAuthDetailsDto {
String token;
UserResponseDto user;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package jewellery.inventory.exception;

import io.jsonwebtoken.security.SignatureException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
Expand All @@ -10,8 +11,10 @@
import jewellery.inventory.exception.invalid_resource_quantity.InvalidResourceQuantityException;
import jewellery.inventory.exception.not_found.NotFoundException;
import jewellery.inventory.exception.not_found.ResourceInUserNotFoundException;
import jewellery.inventory.exception.security.InvalidSecretKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
Expand Down Expand Up @@ -46,6 +49,16 @@ public ResponseEntity<Object> handleBadDataExceptions(RuntimeException ex) {
return createErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage());
}

@ExceptionHandler({SignatureException.class, AuthenticationException.class})
public ResponseEntity<Object> handleAuthenticationException(AuthenticationException ex) {
return createErrorResponse(HttpStatus.UNAUTHORIZED, ex.getMessage());
}

@ExceptionHandler({ InvalidSecretKeyException.class })
public ResponseEntity<Object> handleBadSecretKey(RuntimeException ex) {
return createErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage());
}

private ResponseEntity<Object> createErrorResponse(HttpStatus status, Object error) {
Map<String, Object> body = new HashMap<>();
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_PATTERN));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ public class UserNotFoundException extends NotFoundException {
public UserNotFoundException(UUID id) {
super("User with id " + id + " was not found");
}

public UserNotFoundException(String email) {
super("User with email " + email + " was not found");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package jewellery.inventory.exception.security;

import jewellery.inventory.exception.security.jwt.JwtAuthenticationBaseException;


public class InvalidCredentialsException extends JwtAuthenticationBaseException {
public InvalidCredentialsException() {
super("Invalid credentials");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package jewellery.inventory.exception.security;

import io.jsonwebtoken.security.WeakKeyException;

public class InvalidSecretKeyException extends RuntimeException {
public InvalidSecretKeyException(WeakKeyException e) {
super("Invalid secret key: " + e.getMessage(), e.getCause());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package jewellery.inventory.exception.security.jwt;

import org.springframework.security.core.AuthenticationException;

public class JwtAuthenticationBaseException extends AuthenticationException {
public JwtAuthenticationBaseException(String msg) {
super(msg);
}

public JwtAuthenticationBaseException(String msg, Throwable cause) {
super(msg, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package jewellery.inventory.exception.security.jwt;

public class JwtExpiredException extends JwtAuthenticationBaseException {

public JwtExpiredException() {
super("JWT token has expired");
}
}
Loading

0 comments on commit 687c066

Please sign in to comment.