Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
118 changes: 117 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,120 @@

- Client Service: This holds most of the business domain logic.
- Auth Service: This handles JWT authentication for access to API endpoints.
- API Gateway: This routes requests to allow a single external entrypoint for users. Additionally, it ensures authorization by routing requests to the auth service before hitting other endpoints.
- API Gateway: This routes requests to allow a single external entrypoint for users. Additionally, it ensures authorization by routing requests to the auth service before hitting other endpoints.

# Running the Microservices in IntelliJ

This setup assumes each service will be run through IntelliJ using Dockerfile run configurations.

## General Instructions

1. Make sure Docker Desktop is running.
2. In IntelliJ, create a Dockerfile run configuration for each service:
Edit Configurations -> Add (+) -> Dockerfile.
3. Select the correct Dockerfile for each service and set the required environment variables and run options.
4. Start the database containers first. Then start the services in any order.

---

## API Gateway

**Dockerfile**
api-gateway/Dockerfile

**Environment Variables**
- AUTH_SERVICE_URL=http://auth-service:4005

**Run Options**
- --network coach

---

## Client Service

**Dockerfile**
client-service/Dockerfile

**Environment Variables**
- AUTH_SERVICE_URL=http://auth-service:4005
- SPRING_DATASOURCE_PASSWORD=password
- SPRING_DATASOURCE_URL=jdbc:postgresql://client-service-db:5432/db
- SPRING_DATASOURCE_USERNAME=admin_user
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_SQL_INIT_MODE=always

**Run Options**
- --network coach

---

## Auth Service

**Dockerfile**
auth-service/Dockerfile

**Environment Variables**
- JWT_SECRET={YOUR_JWT_SECRET_HERE}
- SPRING_DATASOURCE_PASSWORD=password
- SPRING_DATASOURCE_URL=jdbc:postgresql://auth-service-db:5432/db
- SPRING_DATASOURCE_USERNAME=admin_user
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_SQL_INIT_MODE=always

**Run Options**
- --network coach

---

## Billing Service (WIP)

**Dockerfile**
billing-service/Dockerfile

**Environment Variables**
- STRIPE_SECRET_KEY={YOUR_STRIPE_SECRET_KEY}

**Run Options**
- --network coach

---

## Auth Service Database

**Base Image**
postgres:latest

**Port Bindings**
- 5002:5432

**Bind Mounts**
.../db_volumes/auth-service-db → /var/lib/postgresql/

**Run Options**
- --network coach

---

## Client Service Database

**Base Image**
postgres:latest

**Port Bindings**
- 5001:5432

**Bind Mounts**
.../db_volumes/client-service-db → /var/lib/postgresql/

**Run Options**
- --network coach

---

## Running Everything

1. Start the database containers:
client-service-db and auth-service-db.
2. Start the application services:
auth-service, client-service, api-gateway, and billing-service.
3. Use IntelliJ run configurations or the Docker CLI.
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.cm.authservice.controller;

import com.cm.authservice.dto.EmailChangeRequestDTO;
import com.cm.authservice.dto.EmailChangeResponseDTO;
import com.cm.authservice.dto.LoginRequestDTO;
import com.cm.authservice.dto.LoginResponseDTO;
import com.cm.authservice.dto.UserRequestDto;
import com.cm.authservice.dto.UserResponseDto;
import com.cm.authservice.model.User;
import com.cm.authservice.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -24,7 +23,7 @@ public AuthController(AuthService authService){

@PostMapping("/login")
@Operation(summary = "Generate token on user login")
public ResponseEntity<LoginResponseDTO> login(@RequestBody LoginRequestDTO loginRequestDTO){
public ResponseEntity<LoginResponseDTO> login(@RequestBody UserRequestDto loginRequestDTO){
Optional<String> tokenOptional = authService.authenticate(loginRequestDTO);

if(tokenOptional.isEmpty()){
Expand All @@ -41,7 +40,6 @@ public ResponseEntity<Void> validateToken(@RequestHeader("Authorization") String
if(authHeader == null || !authHeader.startsWith("Bearer "))
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();


if(authService.validateToken(authHeader.substring(7))){
User user = authService.getUser(authHeader.substring(7));

Expand All @@ -53,18 +51,27 @@ public ResponseEntity<Void> validateToken(@RequestHeader("Authorization") String
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

@PostMapping("/register")
@Operation(summary = "Register a new user")
public ResponseEntity<UserResponseDto> register(@RequestBody UserRequestDto userRequestDto){
UserResponseDto response = authService.register(userRequestDto);
return ResponseEntity.ok().body(response);
}

@PutMapping("/update-email")
@Operation(summary = "Update user account email.")
public ResponseEntity<EmailChangeResponseDTO> updateEmail(@RequestHeader("Authorization") String authHeader,
@RequestBody EmailChangeRequestDTO emailChangeRequestDTO){
public ResponseEntity<UserResponseDto> updateEmail(@RequestHeader("Authorization") String authHeader,
@RequestBody UserRequestDto userRequestDto){

if(authHeader == null || !authHeader.startsWith("Bearer "))
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();

// Now check if the token came from the person who claimed to want to change their email.
EmailChangeResponseDTO emailChangeResponseDTO = authService
.updateEmail(authHeader.substring(7), emailChangeRequestDTO);
UserResponseDto userResponseDto = authService
.updateEmail(authHeader.substring(7), userRequestDto);

return ResponseEntity.ok().body(emailChangeResponseDTO);
return ResponseEntity.ok().body(userResponseDto);
}


}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@Getter
@Setter
public class LoginRequestDTO {
public class UserRequestDto {
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
private String email;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.cm.authservice.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
public class RegistrationResponseDTO {
@Setter
public class UserResponseDto {
private final String email;

public RegistrationResponseDTO(String email){
public UserResponseDto(String email){
this.email = email;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.cm.authservice.mapper;

import com.cm.authservice.dto.UserRequestDto;
import com.cm.authservice.dto.UserResponseDto;
import com.cm.authservice.model.User;

public class UserMapper {

public static User fromDto(UserRequestDto userRequestDto){
User user = new User();
user.setPassword(userRequestDto.getPassword());
user.setEmail(userRequestDto.getEmail());

return user;
}

public static UserResponseDto toDto(User user){
return new UserResponseDto(user.getEmail());
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package com.cm.authservice.service;

import com.cm.authservice.dto.EmailChangeRequestDTO;
import com.cm.authservice.dto.EmailChangeResponseDTO;
import com.cm.authservice.dto.LoginRequestDTO;
import com.cm.authservice.exception.TokenEmailDoesNotMatchException;
import com.cm.authservice.dto.*;
import com.cm.authservice.exception.UserNotFoundException;
import com.cm.authservice.model.User;
import com.cm.authservice.util.JwtUtil;
import io.jsonwebtoken.JwtException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

Expand All @@ -31,7 +30,7 @@ public AuthService(UserService userService,
this.jwtUtil = jwtUtil;
}

public Optional<String> authenticate(LoginRequestDTO loginRequestDTO){
public Optional<String> authenticate(UserRequestDto loginRequestDTO){
log.info("Authenticating a user: {}", loginRequestDTO.getEmail());

// If user password matches, map the user to transform it into a token, which gets assigned to the optional.
Expand All @@ -54,17 +53,13 @@ public boolean validateToken(String token){
}
}

public EmailChangeResponseDTO updateEmail(String token, EmailChangeRequestDTO emailChangeRequestDTO) {
public UserResponseDto updateEmail(String token, UserRequestDto userRequestDto) {
// Validate token and check if it belongs to same person.

User user = userService.findById(jwtUtil.getIdFromToken(token))
.orElseThrow(() -> new UserNotFoundException("User not found."));

if(!user.getEmail().equalsIgnoreCase(emailChangeRequestDTO.getOldEmail())){
throw new TokenEmailDoesNotMatchException("Current account email does not match given old email.");
}

return userService.updateEmail(user, emailChangeRequestDTO);
return userService.updateEmail(user, userRequestDto);
}

public User getUser(String token) {
Expand All @@ -73,4 +68,14 @@ public User getUser(String token) {
return userService.findById(authUserId)
.orElseThrow(() -> new UserNotFoundException("User not found."));
}

public UserResponseDto register(UserRequestDto registrationRequestDto) {
String passwordHash =
BCrypt.hashpw(registrationRequestDto.getPassword(), BCrypt.gensalt());

return
userService.registerUser(registrationRequestDto.getEmail(), passwordHash);


}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.cm.authservice.service;
import com.cm.authservice.dto.UserRequestDto;
import com.cm.authservice.dto.UserResponseDto;
import com.cm.authservice.exception.EmailAlreadyExistsException;
import com.cm.authservice.exception.UserNotFoundException;

import com.cm.authservice.dto.EmailChangeRequestDTO;
import com.cm.authservice.dto.EmailChangeResponseDTO;
import com.cm.authservice.mapper.UserMapper;
import com.cm.authservice.model.User;
import com.cm.authservice.repository.UserRepository;
import org.springframework.stereotype.Service;
Expand All @@ -22,22 +21,33 @@ public Optional<User> findByEmail(String email){
return userRepository.findByEmail(email);
}

public EmailChangeResponseDTO updateEmail(User user, EmailChangeRequestDTO emailChangeRequestDTO){
public UserResponseDto updateEmail(User user, UserRequestDto userRequestDto){

if(userRepository.existsByEmail(emailChangeRequestDTO.getNewEmail())){
throw new EmailAlreadyExistsException("User already exists with email: " + emailChangeRequestDTO.getNewEmail());
if(userRepository.existsByEmail(userRequestDto.getEmail())){
throw new EmailAlreadyExistsException("User already exists with email: " + userRequestDto.getEmail());
}

user.setEmail(emailChangeRequestDTO.getNewEmail());
user.setEmail(userRequestDto.getEmail());
User updatedUser = userRepository.save(user);

EmailChangeResponseDTO emailChangeResponseDTO = new EmailChangeResponseDTO();
emailChangeResponseDTO.setSavedEmail(updatedUser.getEmail());

return emailChangeResponseDTO;
return new UserResponseDto(updatedUser.getEmail());
}

public Optional<User> findById(UUID id) {
return userRepository.findById(id);
}

public UserResponseDto registerUser(String email, String passwordHash) {
if(userRepository.existsByEmail(email)){
throw new EmailAlreadyExistsException("This email is already taken: " + email);
}

User user = new User();
user.setPassword(passwordHash);
user.setEmail(email);

User newUser = userRepository.save(user);

return UserMapper.toDto(newUser);
}
}
Loading