Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solution: A user can login #162

Open
wants to merge 39 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6d10f15
Issue 1: user register and login
khoalun Jun 9, 2024
7a37f37
Add user table and update task table
khoalun Jun 9, 2024
feae4f9
create and login user
khoalun Jun 9, 2024
722480c
Setup JWT util
khoalun Jun 9, 2024
313af00
add test
khoalun Jun 9, 2024
9b366a9
Fix test config for new service
khoalun Jun 9, 2024
3b1e5ea
Small renaming
khoalun Jun 9, 2024
24d5fcd
Refactor
khoalun Jun 9, 2024
d8482dc
Hash password
khoalun Jun 9, 2024
0c47903
Return token after registration or logging in
khoalun Jun 10, 2024
09c63ab
Basic auth
khoalun Jun 10, 2024
4a15c99
Issue and use JWT token with security
khoalun Jun 10, 2024
c345a62
Fix unit tests
khoalun Jun 10, 2024
f59446f
Use jwt token in header of the request to get tasks
khoalun Jun 10, 2024
104fe73
Fix get tasks test
khoalun Jun 10, 2024
edce646
Create task for a user
khoalun Jun 10, 2024
d2c2db1
Fix test create task
khoalun Jun 10, 2024
a66d0ba
Replace email in JWT with JSON stringify of User
khoalun Jun 10, 2024
321b18b
Front: new user component
khoalun Jun 10, 2024
87c13bb
Update npm deps
khoalun Jun 10, 2024
8143949
Front: restructure screens
khoalun Jun 10, 2024
8d567ae
Fix component name
khoalun Jun 10, 2024
1250610
Screens navigation
khoalun Jun 10, 2024
ad7234a
Redirect when not logged in
khoalun Jun 10, 2024
d732b6b
Login form and login handler
khoalun Jun 10, 2024
c987a58
Login and redirect
khoalun Jun 10, 2024
8558f9f
Re-styling
khoalun Jun 10, 2024
ba5ba6b
Logout button
khoalun Jun 10, 2024
119ffea
Save/load auth state from/to local storage
khoalun Jun 10, 2024
4e7bf69
Client util with auth header
khoalun Jun 10, 2024
70f4f5a
Register and then login
khoalun Jun 10, 2024
157310c
Clean up
khoalun Jun 10, 2024
8851360
Fix existing tests
khoalun Jun 10, 2024
b03a00a
Error message when failed login
khoalun Jun 10, 2024
d0d7875
Fix DB migration script
khoalun Jun 10, 2024
219c3a1
Fix naming
khoalun Jun 10, 2024
9cde5b1
Handle duplicate email message when registering
khoalun Jun 10, 2024
6732239
BE: User controller and service tests
khoalun Jun 11, 2024
0cf2eda
Front: e2e test
khoalun Jun 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,17 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.flywaydb:flyway-core'
implementation 'org.modelmapper:modelmapper:3.2.0'
implementation 'org.postgresql:postgresql'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'commons-codec:commons-codec:1.17.0'
implementation 'com.google.code.gson:gson'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
36 changes: 18 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,30 @@
"clean": "rm -rf ./dist ./node-modules ./src/main/coverage"
},
"dependencies": {
"@angular/animations": "~17.3.9",
"@angular/cdk": "~17.3.9",
"@angular/common": "~17.3.9",
"@angular/compiler": "~17.3.9",
"@angular/core": "~17.3.9",
"@angular/forms": "~17.3.9",
"@angular/material": "~17.3.9",
"@angular/platform-browser": "~17.3.9",
"@angular/platform-browser-dynamic": "~17.3.9",
"@angular/router": "~17.3.9",
"@angular/animations": "18.0.2",
"@angular/cdk": "18.0.2",
"@angular/common": "18.0.2",
"@angular/compiler": "18.0.2",
"@angular/core": "18.0.2",
"@angular/forms": "18.0.2",
"@angular/material": "18.0.2",
"@angular/platform-browser": "18.0.2",
"@angular/platform-browser-dynamic": "18.0.2",
"@angular/router": "18.0.2",
"rxjs": "~7.8.1",
"tslib": "^2.3.0",
"uuid": "^9.0.1",
"zone.js": "~0.14.6"
},
"devDependencies": {
"@angular-devkit/build-angular": "~17.3.7",
"@angular-eslint/builder": "17.4.1",
"@angular-eslint/eslint-plugin": "17.4.1",
"@angular-eslint/eslint-plugin-template": "17.4.1",
"@angular-eslint/schematics": "17.4.1",
"@angular-eslint/template-parser": "17.4.1",
"@angular/cli": "~17.3.7",
"@angular/compiler-cli": "~17.3.9",
"@angular-devkit/build-angular": "^18.0.3",
"@angular-eslint/builder": "^18.0.1",
"@angular-eslint/eslint-plugin": "^18.0.1",
"@angular-eslint/eslint-plugin-template": "^18.0.1",
"@angular-eslint/schematics": "^18.0.1",
"@angular-eslint/template-parser": "^18.0.1",
"@angular/cli": "^18.0.3",
"@angular/compiler-cli": "^18.0.2",
"@cypress/schematic": "2.5.1",
"@types/jasmine": "~5.1.4",
"@types/node": "^20.12.12",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.coyoapp.tinytask.configuration;

import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.web.SecurityFilterChain;

import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

import com.nimbusds.jose.proc.SecurityContext;


@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Value("${jwt.public.key}")
RSAPublicKey key;

@Value("${jwt.private.key}")
RSAPrivateKey prKey;

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/", "/users/login", "/users/register");
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((auth) -> auth
.anyRequest().authenticated()
)
.oauth2ResourceServer((oauth2ResourceServer) ->
oauth2ResourceServer
.jwt((jwt) -> jwt.decoder(jwtDecoder())
)
);
return http.build();
}

@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.key).build();
}

@Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(this.key).privateKey(this.prKey).build();
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwkSource);
}
}
12 changes: 6 additions & 6 deletions src/main/java/com/coyoapp/tinytask/domain/Task.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package com.coyoapp.tinytask.domain;

import java.time.Instant;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
Expand All @@ -30,4 +26,8 @@ public class Task {

@CreatedDate
private Instant created;

@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
}
46 changes: 46 additions & 0 deletions src/main/java/com/coyoapp/tinytask/domain/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.coyoapp.tinytask.domain;

import com.google.gson.annotations.Expose;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

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

@Setter
@Getter
@Entity
@Table(name = "\"user\"") // Use double quotes to handle reserved keywords
public class User {

public User() {

}

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

@Expose
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Expose
@Column(nullable = false, length = 128)
private String email;

@Column(nullable = false, length = 256)
private String password;

@Expose
@Column(nullable = false, updatable = false, insertable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime created;

@OneToMany(mappedBy = "user")
private List<Task> tasks;


}
17 changes: 17 additions & 0 deletions src/main/java/com/coyoapp/tinytask/dto/user/UserRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.coyoapp.tinytask.dto.user;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import jakarta.validation.constraints.NotEmpty;

@Setter
@Getter
@Builder
public class UserRequest {
@NotEmpty
private String email;

@NotEmpty
private String password;
}
29 changes: 29 additions & 0 deletions src/main/java/com/coyoapp/tinytask/dto/user/UserResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.coyoapp.tinytask.dto.user;

import jakarta.validation.constraints.NotEmpty;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Builder
public class UserResponse {

public UserResponse() {}

public UserResponse(long id, String email, String jwtToken) {
this.id = id;
this.email = email;
this.jwtToken = jwtToken;
}

@NotEmpty
private long id;

@NotEmpty
private String email;

@NotEmpty
private String jwtToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.coyoapp.tinytask.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
import com.coyoapp.tinytask.domain.Task;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface TaskRepository extends JpaRepository<Task, String> {
List<Task> findAllByUserId(long userId);
}
12 changes: 12 additions & 0 deletions src/main/java/com/coyoapp/tinytask/repository/UserRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.coyoapp.tinytask.repository;

import com.coyoapp.tinytask.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, String> {
Optional<User> findByEmailAndPassword(String email, String password);
Optional<User> findByEmail(String email);
Optional<User> findById(long id);
}
18 changes: 13 additions & 5 deletions src/main/java/com/coyoapp/tinytask/service/DefaultTaskService.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.coyoapp.tinytask.service;

import com.coyoapp.tinytask.domain.Task;
import com.coyoapp.tinytask.domain.User;
import com.coyoapp.tinytask.dto.TaskRequest;
import com.coyoapp.tinytask.dto.TaskResponse;
import com.coyoapp.tinytask.exception.TaskNotFoundException;
import com.coyoapp.tinytask.exception.UserNotFoundException;
import com.coyoapp.tinytask.repository.TaskRepository;

import java.util.List;

import com.coyoapp.tinytask.service.user.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
Expand All @@ -21,20 +26,23 @@ public class DefaultTaskService implements TaskService {

private final TaskRepository taskRepository;
private final ModelMapper mapper;
private final UserService userService;

@Override
@Transactional
public TaskResponse createTask(TaskRequest taskRequest) {
log.debug("createTask(createTask={})", taskRequest);
public TaskResponse createTask(TaskRequest taskRequest, long userId) {
log.debug("createTask(createTask={} userId={})", taskRequest, userId);
Task task = mapper.map(taskRequest, Task.class);
User user = userService.findById(userId).orElseThrow(UserNotFoundException::new);
task.setUser(user);
return transformToResponse(taskRepository.save(task));
}

@Override
@Transactional(readOnly = true)
public List<TaskResponse> getTasks() {
log.debug("getTasks()");
return taskRepository.findAll().stream().map(this::transformToResponse).collect(toList());
public List<TaskResponse> getTasks(long userId) {
log.debug("getTasks(userId={})", userId);
return taskRepository.findAllByUserId(userId).stream().map(this::transformToResponse).collect(toList());
}

private TaskResponse transformToResponse(Task task) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/coyoapp/tinytask/service/TaskService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

public interface TaskService {

TaskResponse createTask(TaskRequest taskRequest);
TaskResponse createTask(TaskRequest taskRequest, long userId);

List<TaskResponse> getTasks();
List<TaskResponse> getTasks(long userId);

void deleteTask(String taskId);

Expand Down
Loading