Skip to content

Commit

Permalink
Merge branch 'main' into SCRUM-95
Browse files Browse the repository at this point in the history
  • Loading branch information
Hsbalazs authored Sep 9, 2024
2 parents f0b3bf8 + 340b71d commit a624d2d
Show file tree
Hide file tree
Showing 22 changed files with 279 additions and 55 deletions.
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MAIL_PASSWORD=
MAIL_USERNAME=
38 changes: 38 additions & 0 deletions .github/workflows/tag-based-thing.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Setup an action to run the frontend CI for PRs

# name of the action
name: Frontend CI

# when should it run
on:
push:
tags:
- v**

# what should it do
jobs:
# name of the job
build:
# what should it run on
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
# default values for all the steps
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm install
- name: Run build
run: npm run build
- name: Run tests
run: npm test
- name: Run formatter + linter
run: npm run check
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ data
*.idea

.DS_Store
.env
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,21 @@

You can find the frontend of the project in the `frontend` folder
[here](./frontend/README.md).

### Feature toggles

| Feature | Description | Environment Variable | Default |
| ------------------- | -------------------------------------- | ---------------------------------- | ------- |
| email verificiation | Email verification during registration | FEATURE_EMAIL_VERIFICATION_ENABLED | false |

## Environemnts and secrets

The project uses a `.env` file to store the secrets and environment variables. You can find an example of the file in the `.env.example` file. The dotenv file is used to store the secrets and environment variables for the docker-compose file.

You can copy the sample file to a new file called `.env` and fill in the values.

```bash
cp .env.sample .env
```

then fill in the values in the `.env` file.
4 changes: 4 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@
<artifactId>spring-boot-starter-mail</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

/**
* This is the main class of the application.
*/
@SpringBootApplication
@EnableCaching
public class BackendApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.greenfoxacademy.backend.config;

import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

/**
* Configuration class for feature flags.
*/
@Getter
@Configuration
public class FeatureFlags {

/**
* Flag to enable email verification.
*/
@Value("${features.emailVerificationEnabled}")
private boolean emailVerificationEnabled;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.greenfoxacademy.backend.config;

import java.time.Duration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
* RedisConfig is a configuration class that defines the cache management strategy
* for Redis in the Spring Boot application. It configures the Redis Cache Manager
* and sets key serialization and expiration policies.
*/
@Configuration
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis")
public class RedisConfig {

/**
* The cacheManager method creates a RedisCacheManager bean that is used to manage.
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(3)) // Set TTL to 5 minutes
.disableCachingNullValues()
.serializeKeysWith(
RedisSerializationContext
.SerializationPair
.fromSerializer(new StringRedisSerializer())
);

return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfig)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*
* @param token the token that is returned to identify the user
*/

public record LoginResponseDto(
String token
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jakarta.mail.internet.MimeMessage;
import java.util.UUID;
import lombok.RequiredArgsConstructor;

import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.greenfoxacademy.backend.services.user.owner;

import com.greenfoxacademy.backend.config.FeatureFlags;
import com.greenfoxacademy.backend.dtos.LoginRequestDto;
import com.greenfoxacademy.backend.dtos.LoginResponseDto;
import com.greenfoxacademy.backend.dtos.ProfileUpdateRequestDto;
Expand All @@ -14,8 +15,12 @@
import com.greenfoxacademy.backend.services.mail.EmailService;
import com.greenfoxacademy.backend.services.user.owner.OwnerService;
import jakarta.transaction.Transactional;

import java.util.UUID;

import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
Expand All @@ -31,34 +36,37 @@ public class OwnerServiceImpl implements OwnerService {
private final PasswordEncoder passwordEncoder;
private final AuthService authService;
private final EmailService emailService;
private final FeatureFlags featureFlags;

@Override
public RegisterResponseDto register(RegisterRequestDto registerRequestDto)
throws UserAlreadyExistsError {

boolean isEmailEnabled = featureFlags.isEmailVerificationEnabled();
UUID verificationId = isEmailEnabled ? UUID.randomUUID() : null;
// @formatter:off
Owner owner = Owner.builder()
.email(registerRequestDto.email())
.firstName(registerRequestDto.firstName())
.lastName(registerRequestDto.lastName())
.password(passwordEncoder.encode(registerRequestDto.password()))
.verificationId(UUID.randomUUID())
.verificationId(verificationId)
.build();
// @formatter:on
try {
Owner saved = ownerRepository.save(owner);
emailService.sendRegistrationEmail(
saved.getEmail(),
saved.getFirstName(),
saved.getVerificationId()
);
if (isEmailEnabled) {
emailService.sendRegistrationEmail(
saved.getEmail(),
saved.getFirstName(),
saved.getVerificationId());
}
return new RegisterResponseDto(saved.getId());
} catch (Exception e) {
throw new UserAlreadyExistsError("Email is already taken!");
}
}


@Override
public LoginResponseDto login(LoginRequestDto loginRequestDto) throws Exception {
Owner owner = ownerRepository.findByEmail(loginRequestDto.email())
Expand All @@ -71,6 +79,7 @@ public LoginResponseDto login(LoginRequestDto loginRequestDto) throws Exception
return new LoginResponseDto(authService.generateToken(owner));
}

@CacheEvict(value = "update-profile-cache", key = "#email")
@Override
public ProfileUpdateResponseDto profileUpdate(
String email,
Expand All @@ -93,14 +102,17 @@ public ProfileUpdateResponseDto profileUpdate(
return new ProfileUpdateResponseDto(authService.generateToken(updatedUser));
}

public Owner findByEmail(String username) throws UsernameNotFoundException {
@Cacheable(value = "profile-cache", key = "#username")
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return ownerRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("No such user!"));
}

/**
* Delete the user by username.
*/
@CacheEvict(value = "delete-cache", key = "#username")
@Transactional
@Override
public void deleteProfile(String username) {
Expand All @@ -112,6 +124,9 @@ public void deleteProfile(String username) {
*/
public void verifyUser(UUID id) {
Owner userWithId = ownerRepository.findByVerificationId(id).orElseThrow();
if (!featureFlags.isEmailVerificationEnabled()) {
return;
}
userWithId.setVerificationId(null);
ownerRepository.save(userWithId);
}
Expand Down
11 changes: 10 additions & 1 deletion backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,13 @@ spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

# Where the verification link will redirect
email.baseUrl=${EMAIL_BASEURL:http://localhost:8080}
email.baseUrl=${EMAIL_BASEURL:http://localhost:8080}

# Redis configuration
spring.data.redis.host=${CACHE_URL:localhost}
spring.data.redis.port=${CACHE_PORT:6379}
spring.data.redis.password=${CACHE_PASSWORD}
spring.cache.type=redis

# Feature Flags
features.emailVerificationEnabled=${FEATURE_EMAIL_VERIFICATION_ENABLED:true}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

import com.greenfoxacademy.backend.config.FeatureFlags;
import com.greenfoxacademy.backend.dtos.LoginRequestDto;
import com.greenfoxacademy.backend.dtos.ProfileUpdateRequestDto;
import com.greenfoxacademy.backend.dtos.RegisterRequestDto;
Expand Down Expand Up @@ -40,12 +41,19 @@ class OwnerServiceImplTest {
private AuthService authService;
@Mock
private EmailService emailService;
@Mock
private FeatureFlags featureFlags;


@BeforeEach
void setUp() {
Mockito.reset(ownerRepository);
userService = new OwnerServiceImpl(ownerRepository, passwordEncoder, authService, emailService);
userService = new OwnerServiceImpl(
ownerRepository,
passwordEncoder,
authService,
emailService,
featureFlags);
}

@DisplayName("Register a new user if email not taken")
Expand Down Expand Up @@ -215,6 +223,7 @@ void loadUserByUsername() {

@Test
void verifyUserById() {
when(featureFlags.isEmailVerificationEnabled()).thenReturn(true);
UUID id = UUID.randomUUID();
Owner owner = Owner.builder()
.id(1)
Expand Down
8 changes: 7 additions & 1 deletion backend/src/test/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@ spring.mail.properties.mail.smtp.starttls.enable=true
cors.urls=${CORS_URLS:http://localhost:5173}

# Where the verification link will redirect
email.baseUrl=${EMAIL_BASEURL:http://localhost:8080}
email.baseUrl=${EMAIL_BASEURL:http://localhost:8080}

# Redis configuration
spring.cache.type=none

# Feature Flags
features.emailVerificationEnabled=${FEATURE_EMAIL_VERIFICATION_ENABLED:true}
12 changes: 12 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ services:
POSTGRES_PASSWORD: petclinic_db_password
POSTGRES_DB: petclinic_db

redis-cache:
image: bitnami/redis
environment:
- REDIS_PASSWORD=some_redis_password
ports:
- 6379:6379

frontend:
build:
context: ./frontend
Expand All @@ -28,5 +35,10 @@ services:
- DATABASE_USER=petclinic_db_user
- DATABASE_PASSWORD=petclinic_db_password
- CORS_URLS=http://localhost:5173
- FEATURE_EMAIL_VERIFICATION_ENABLED=false
- CACHE_PORT=6379
- CACHE_URL=redis://redis-cache
- CACHE_PASSWORD=some_redis_password
depends_on:
- postgres-db
- redis-cache
2 changes: 1 addition & 1 deletion frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#create frontend container
FROM node:lts as builder
FROM node:lts AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
Expand Down
Loading

0 comments on commit a624d2d

Please sign in to comment.