Skip to content

Commit

Permalink
implement rudimentary user management
Browse files Browse the repository at this point in the history
for now on basic - should be token-based soon
  • Loading branch information
wisskirchenj committed Jan 2, 2024
1 parent f37e1c6 commit 609e01d
Show file tree
Hide file tree
Showing 22 changed files with 428 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .idea/checkstyle-idea.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions .idea/jarRepositories.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
services:
mongodb:
image: 'mongo:latest'
environment:
- 'MONGO_INITDB_DATABASE=mydatabase'
- 'MONGO_INITDB_ROOT_PASSWORD=secret'
- 'MONGO_INITDB_ROOT_USERNAME=root'
ports:
- '27017:27017'
volumes:
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/HelloWorld.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>

<h1 class="text-h2 font-weight-bold">Vuetify</h1>
<h1 class="text-h2 font-weight-bold">Flashcards</h1>

<div class="py-14" />

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.hyperskill.community.flashcards;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@Slf4j
public class FlashcardsApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.hyperskill.community.flashcards.config;

import com.mongodb.client.MongoClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;

@Configuration
public class MongoConfiguration {

@Bean
public MongoTemplate mongoTemplate() {
return new MongoTemplate(MongoClients.create(), "cards");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.hyperskill.community.flashcards.config;

import jakarta.servlet.Filter;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.context.SecurityContextHolder;

@Configuration
public class ObservabilityConfiguration {

@Bean
Filter correlationFilter() {
return (request, response, chain) -> {
var loggedIn = SecurityContextHolder.getContext().getAuthentication();
MDC.put("user", loggedIn.getName());
chain.doFilter(request, response);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.hyperskill.community.flashcards.config;

import org.hyperskill.community.flashcards.registration.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.http.HttpMethod;
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.configurers.CsrfConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import java.util.Optional;

import static org.springframework.security.config.Customizer.withDefaults;

/**
* new Spring security 6.0 style provision of SecurityFilterChain bean with the security configuration,
* as well as PasswordProvider and AuthenticationManager that makes use of our UserDetails persistence.
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(CsrfConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/register.html", "/js/register.js", "/css/register.css").permitAll()
.requestMatchers(HttpMethod.POST, "/api/register").permitAll()
.anyRequest().authenticated())
.httpBasic(withDefaults())
.build();
}

@Bean
public UserDetailsService userDetailsService(MongoTemplate mongoTemplate) {
return username ->
Optional.ofNullable(mongoTemplate.findById(username, User.class))
.orElseThrow(() -> new UsernameNotFoundException("User not found."));
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.hyperskill.community.flashcards.model;

public record Collection(String name) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.hyperskill.community.flashcards.registration;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.http.ResponseEntity.ok;

@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/api/register")
public class RegisterController {

private final RegisterService service;
private final UserMapper mapper;

/**
* register endpoint - unauthenticated (!).
* @param userDto dto containing provided user email (=username) and raw password
* @return empty response 200(OK) on successful register, 400(BadRequest) if dto validation fails or user exists
*/
@PostMapping
public ResponseEntity<Void> registerUser(@Valid @RequestBody UserDto userDto) {
var userDocument = mapper.toDocument(userDto);
service.registerUser(userDocument);
log.info("User {} successfully registered", userDocument.getUsername());
return ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.hyperskill.community.flashcards.registration;

import lombok.RequiredArgsConstructor;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;

import java.util.Objects;

@Service
@RequiredArgsConstructor
public class RegisterService {

private final MongoTemplate mongoTemplate;

/**
* method receives and saves the User entity with data mapped from the UserDto (name and encrypted password),
* @param user the prepared User entity to save to the database.
* @throws UserAlreadyExistsException if user already exists.
*/
public void registerUser(User user) throws UserAlreadyExistsException {
if (Objects.nonNull(mongoTemplate.findById(user.getUsername(), User.class))) {
throw new UserAlreadyExistsException();
}
mongoTemplate.save(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.hyperskill.community.flashcards.registration;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;

/**
* entity class for registered users, that implements UserDetails and whose instances thus serve the
* DaoAuthenticationProvider (AuthenticationManager).
*/
@Getter
@Setter
@RequiredArgsConstructor
@Accessors(chain = true)
@Document
public class User implements UserDetails {
@Id
private String username;
private String password;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptyList();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.hyperskill.community.flashcards.registration;

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

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class UserAlreadyExistsException extends RuntimeException {

public UserAlreadyExistsException() {
super("This user is already registered.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.hyperskill.community.flashcards.registration;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

/**
* immutable web-layer DTO as carrier for user register requests.
*/
public record UserDto(@NotNull @Pattern(regexp = "\\w+(\\.\\w+){0,2}@\\w+\\.\\w+") String email,
@NotBlank @Size(min = 8) String password
) { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.hyperskill.community.flashcards.registration;

import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;


/**
* mapper to map received UserDto on register to a User entity, hereby encoding the raw password.
*/
@Component
@RequiredArgsConstructor
public class UserMapper {

private final PasswordEncoder passwordEncoder;

/**
* map the Dto to the entity and encode the password hereby.
*/
public User toDocument(UserDto dto) {
return new User().setUsername(dto.email())
.setPassword(passwordEncoder.encode(dto.password()));
}
}
2 changes: 1 addition & 1 deletion server/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@

spring.data.mongodb.database=cards
18 changes: 18 additions & 0 deletions server/src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<appender name="Console"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{ISO8601} %highlight(%-5level) [%blue(%t)] %yellow(%C{1}): &lt;%X{user}&gt; %msg%n%throwable
</pattern>
</encoder>
</appender>

<!-- LOG everything at INFO level -->
<root level="info">
<appender-ref ref="Console" />
</root>

</configuration>
Loading

0 comments on commit 609e01d

Please sign in to comment.