perf: azure deployment optimizations
This commit is contained in:
62
.github/workflows/deploy.yml
vendored
62
.github/workflows/deploy.yml
vendored
@@ -9,21 +9,53 @@ env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
build-backend:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set image names (lowercase)
|
||||
- name: Set image name (lowercase)
|
||||
run: |
|
||||
OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||
echo "BACKEND_IMAGE=ghcr.io/${OWNER}/petshop-backend" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push backend image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./backend
|
||||
push: true
|
||||
tags: ${{ env.BACKEND_IMAGE }}:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set image name (lowercase)
|
||||
run: |
|
||||
OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||
echo "FRONTEND_IMAGE=ghcr.io/${OWNER}/petshop-web" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
@@ -33,12 +65,8 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push backend image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./backend
|
||||
push: true
|
||||
tags: ${{ env.BACKEND_IMAGE }}:latest
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push frontend image
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -48,6 +76,22 @@ jobs:
|
||||
tags: ${{ env.FRONTEND_IMAGE }}:latest
|
||||
build-args: |
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-backend, build-frontend]
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Set image names (lowercase)
|
||||
run: |
|
||||
OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||
echo "BACKEND_IMAGE=ghcr.io/${OWNER}/petshop-backend" >> $GITHUB_ENV
|
||||
echo "FRONTEND_IMAGE=ghcr.io/${OWNER}/petshop-web" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: azure/login@v2
|
||||
|
||||
@@ -10,4 +10,4 @@ FROM eclipse-temurin:25-jre
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/target/*.jar app.jar
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java","-jar","app.jar"]
|
||||
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-XX:+UseG1GC", "-jar", "app.jar"]
|
||||
|
||||
@@ -38,6 +38,16 @@
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
|
||||
@@ -4,10 +4,12 @@ import com.petshop.backend.config.FlywayContextInitializer;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.data.web.config.EnableSpringDataWebSupport;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@EnableAsync
|
||||
@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)
|
||||
public class BackendApplication {
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.petshop.backend.config;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfig {
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
CaffeineCacheManager manager = new CaffeineCacheManager("userAuthCache");
|
||||
manager.setCaffeine(Caffeine.newBuilder()
|
||||
.expireAfterWrite(60, TimeUnit.SECONDS)
|
||||
.maximumSize(1000));
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import com.petshop.backend.entity.StoreLocation;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import com.petshop.backend.security.JwtUtil;
|
||||
import com.petshop.backend.security.UserAuthCacheService;
|
||||
import com.petshop.backend.service.ActivityLogService;
|
||||
import com.petshop.backend.service.AvatarStorageService;
|
||||
import com.petshop.backend.service.EmailService;
|
||||
@@ -58,8 +59,9 @@ public class AuthController {
|
||||
private final ActivityLogService activityLogService;
|
||||
private final PasswordResetService passwordResetService;
|
||||
private final EmailService emailService;
|
||||
private final UserAuthCacheService userAuthCacheService;
|
||||
|
||||
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService, PasswordResetService passwordResetService, EmailService emailService) {
|
||||
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService, PasswordResetService passwordResetService, EmailService emailService, UserAuthCacheService userAuthCacheService) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.userRepository = userRepository;
|
||||
this.jwtUtil = jwtUtil;
|
||||
@@ -68,6 +70,7 @@ public class AuthController {
|
||||
this.activityLogService = activityLogService;
|
||||
this.passwordResetService = passwordResetService;
|
||||
this.emailService = emailService;
|
||||
this.userAuthCacheService = userAuthCacheService;
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
@@ -263,6 +266,7 @@ public class AuthController {
|
||||
error.put("message", "Username, email, or phone already exists");
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
|
||||
}
|
||||
userAuthCacheService.evict(updatedUser.getId());
|
||||
return ResponseEntity.ok(toUserInfoResponse(updatedUser));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.petshop.backend.security;
|
||||
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.exception.ApiErrorResponder;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
@@ -16,16 +15,18 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
private final UserRepository userRepository;
|
||||
private final UserAuthCacheService userAuthCacheService;
|
||||
private final ApiErrorResponder apiErrorResponder;
|
||||
|
||||
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserRepository userRepository, ApiErrorResponder apiErrorResponder) {
|
||||
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserAuthCacheService userAuthCacheService, ApiErrorResponder apiErrorResponder) {
|
||||
this.jwtUtil = jwtUtil;
|
||||
this.userRepository = userRepository;
|
||||
this.userAuthCacheService = userAuthCacheService;
|
||||
this.apiErrorResponder = apiErrorResponder;
|
||||
}
|
||||
|
||||
@@ -44,30 +45,44 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
jwt = authHeader.substring(7);
|
||||
Long userId;
|
||||
String username;
|
||||
String roleStr;
|
||||
Integer jwtTokenVersion;
|
||||
try {
|
||||
userId = jwtUtil.extractUserId(jwt);
|
||||
username = jwtUtil.extractUsername(jwt);
|
||||
roleStr = jwtUtil.extractRole(jwt);
|
||||
jwtTokenVersion = jwtUtil.extractTokenVersion(jwt);
|
||||
} catch (JwtException | IllegalArgumentException ex) {
|
||||
writeUnauthorized(request, response, "Invalid or expired token", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
User user = userRepository.findById(userId).orElse(null);
|
||||
if (user == null || user.getActive() == null || !user.getActive()) {
|
||||
writeUnauthorized(request, response, "User account is inactive", null);
|
||||
return;
|
||||
}
|
||||
if (!jwtUtil.validateToken(jwt, user)) {
|
||||
if (jwtUtil.extractExpiration(jwt).before(new Date())) {
|
||||
writeUnauthorized(request, response, "Invalid or expired token", null);
|
||||
return;
|
||||
}
|
||||
|
||||
AppPrincipal principal = new AppPrincipal(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getRole(),
|
||||
user.getTokenVersion()
|
||||
);
|
||||
UserAuthCacheService.UserAuthData authData = userAuthCacheService.loadAuthData(userId);
|
||||
if (authData == null || !Boolean.TRUE.equals(authData.active())) {
|
||||
writeUnauthorized(request, response, "User account is inactive", null);
|
||||
return;
|
||||
}
|
||||
if (!authData.tokenVersion().equals(jwtTokenVersion)) {
|
||||
writeUnauthorized(request, response, "Invalid or expired token", null);
|
||||
return;
|
||||
}
|
||||
|
||||
User.Role role;
|
||||
try {
|
||||
role = User.Role.valueOf(roleStr);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
writeUnauthorized(request, response, "Invalid or expired token", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
AppPrincipal principal = new AppPrincipal(userId, username, role, jwtTokenVersion);
|
||||
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
||||
principal,
|
||||
null,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.petshop.backend.security;
|
||||
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UserAuthCacheService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserAuthCacheService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
public record UserAuthData(Boolean active, Integer tokenVersion) {}
|
||||
|
||||
@Cacheable(value = "userAuthCache", key = "#userId")
|
||||
public UserAuthData loadAuthData(Long userId) {
|
||||
return userRepository.findById(userId)
|
||||
.map(u -> new UserAuthData(u.getActive(), u.getTokenVersion()))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@CacheEvict(value = "userAuthCache", key = "#userId")
|
||||
public void evict(Long userId) {
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@@ -18,6 +19,7 @@ public class ActivityLogService {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Async
|
||||
public void record(Long userId, String activity) {
|
||||
if (userId == null || activity == null || activity.isBlank()) {
|
||||
return;
|
||||
@@ -36,6 +38,7 @@ public class ActivityLogService {
|
||||
}
|
||||
}
|
||||
|
||||
@Async
|
||||
public void record(User user, String activity) {
|
||||
if (user == null) {
|
||||
return;
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.exception.BusinessException;
|
||||
import com.petshop.backend.repository.PasswordResetTokenRepository;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import com.petshop.backend.security.UserAuthCacheService;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -29,16 +30,19 @@ public class PasswordResetService {
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final EmailService emailService;
|
||||
private final UserAuthCacheService userAuthCacheService;
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
public PasswordResetService(PasswordResetTokenRepository passwordResetTokenRepository,
|
||||
UserRepository userRepository,
|
||||
PasswordEncoder passwordEncoder,
|
||||
EmailService emailService) {
|
||||
EmailService emailService,
|
||||
UserAuthCacheService userAuthCacheService) {
|
||||
this.passwordResetTokenRepository = passwordResetTokenRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.emailService = emailService;
|
||||
this.userAuthCacheService = userAuthCacheService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -97,6 +101,7 @@ public class PasswordResetService {
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
user.setTokenVersion(user.getTokenVersion() + 1);
|
||||
userRepository.save(user);
|
||||
userAuthCacheService.evict(user.getId());
|
||||
|
||||
token.setUsedAt(now);
|
||||
passwordResetTokenRepository.save(token);
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||
import com.petshop.backend.repository.StoreRepository;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import com.petshop.backend.security.UserAuthCacheService;
|
||||
import com.petshop.backend.util.AuthenticationHelper;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -33,11 +34,13 @@ public class UserService {
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final StoreRepository storeRepository;
|
||||
private final UserAuthCacheService userAuthCacheService;
|
||||
|
||||
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, StoreRepository storeRepository) {
|
||||
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, StoreRepository storeRepository, UserAuthCacheService userAuthCacheService) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.storeRepository = storeRepository;
|
||||
this.userAuthCacheService = userAuthCacheService;
|
||||
}
|
||||
|
||||
public Page<UserResponse> getAllUsers(String query, String role, Pageable pageable) {
|
||||
@@ -147,6 +150,7 @@ public class UserService {
|
||||
}
|
||||
|
||||
user = userRepository.save(user);
|
||||
userAuthCacheService.evict(user.getId());
|
||||
return mapToResponse(user);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ spring:
|
||||
username: ${SPRING_DATASOURCE_USERNAME:petshop}
|
||||
password: ${SPRING_DATASOURCE_PASSWORD:petshop}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
|
||||
sql:
|
||||
init:
|
||||
@@ -42,6 +46,10 @@ server:
|
||||
address: ${SERVER_ADDRESS:0.0.0.0}
|
||||
servlet:
|
||||
context-path: /
|
||||
compression:
|
||||
enabled: true
|
||||
mime-types: application/json,application/javascript,text/css,text/plain
|
||||
min-response-size: 1024
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
|
||||
Reference in New Issue
Block a user