diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c1cafed6..ef7b125a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,84 +39,36 @@ jobs: - run: npm --prefix webapp install - run: npm --prefix webapp run build - run: npm --prefix webapp run test:e2e - docker-push-webapp: - name: Push webapp Docker Image to GitHub Packages + docker-push-api: runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [e2e-tests] + needs: [ e2e-tests ] steps: - - uses: actions/checkout@v4 - - name: Publish to Registry - uses: elgohr/Publish-Docker-Github-Action@v5 - env: - API_URI: http://${{ secrets.DEPLOY_HOST }}:8000 - with: - name: arquisoft/wiq_en2b/webapp + - uses: actions/checkout@v4 + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@v5 + env: + API_URI: http://${{ secrets.DEPLOY_HOST }}:8080 + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + with: + name: arquisoft/wiq_en2b/api username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} registry: ghcr.io - workdir: webapp + workdir: api buildargs: API_URI - docker-push-authservice: - name: Push auth service Docker Image to GitHub Packages - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [e2e-tests] - steps: - - uses: actions/checkout@v4 - - name: Publish to Registry - uses: elgohr/Publish-Docker-Github-Action@v5 - with: - name: arquisoft/wiq_en2b/authservice - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - workdir: users/authservice - docker-push-userservice: - name: Push user service Docker Image to GitHub Packages - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [e2e-tests] - steps: - - uses: actions/checkout@v4 - - name: Publish to Registry - uses: elgohr/Publish-Docker-Github-Action@v5 - with: - name: arquisoft/wiq_en2b/userservice - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - workdir: users/userservice - docker-push-gatewayservice: - name: Push gateway service Docker Image to GitHub Packages - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [e2e-tests] - steps: - - uses: actions/checkout@v4 - - name: Publish to Registry - uses: elgohr/Publish-Docker-Github-Action@v5 - with: - name: arquisoft/wiq_en2b/gatewayservice - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - workdir: gatewayservice deploy: name: Deploy over SSH runs-on: ubuntu-latest - needs: [docker-push-userservice,docker-push-authservice,docker-push-gatewayservice,docker-push-webapp] + needs: [docker-push-api] steps: - name: Deploy over SSH uses: fifsky/ssh-action@master + env: + DATABASE_USER: ${{ secrets.DATABASE_USER }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} with: host: ${{ secrets.DEPLOY_HOST }} user: ${{ secrets.DEPLOY_USER }} diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 00000000..0e30a21e --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,23 @@ +FROM maven:3.8.1-openjdk-17 AS build +# Compile api +WORKDIR /api +COPY . /api +WORKDIR /api +RUN mvn install +RUN mvn clean package + +FROM amazoncorretto:17 AS runtime +# Copy the compiled jar file from the build stage +ARG DATABASE_USER +ARG DATABASE_PASSWORD +ARG JWT_SECRET + +# Set environment variables +ENV DATABASE_URL jdbc:postgresql://WIQ_DB:5432/wiq +ENV DATABASE_USER $DATABASE_USER +ENV DATABASE_PASSWORD $DATABASE_PASSWORD +ENV JWT_SECRET $JWT_SECRET +COPY --from=build /api/target/quiz-api-0.0.1-SNAPSHOT.jar app.jar +ENTRYPOINT ["java","-jar","app.jar"] + +EXPOSE 8080 \ No newline at end of file diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java index 1a311a0a..8ac55d08 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthController.java @@ -4,7 +4,7 @@ import lab.en2b.quizapi.auth.dtos.LoginDto; import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; import lab.en2b.quizapi.auth.dtos.RegisterDto; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -12,10 +12,9 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/auth") +@RequiredArgsConstructor public class AuthController { - - @Autowired - private AuthService authService; + private final AuthService authService; @PostMapping("/register") public ResponseEntity registerUser(@Valid @RequestBody RegisterDto registerRequest){ diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 82e39000..888d946d 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -46,11 +46,22 @@ public ResponseEntity login(LoginDto loginRequest){ ); } + /** + * Registers a user. Throws an 400 unauthorized exception otherwise + * @param registerRequest the request containing the register info + * @return a response containing a message + */ public ResponseEntity register(RegisterDto registerRequest) { userService.createUser(registerRequest,Set.of("user")); return ResponseEntity.ok("User registered successfully!"); } + /** + * Refreshes the jwt token. Throws an 404 unauthorized exception if the refresh token is not in the database or + * an 400 unauthorized exception if the refresh token is not valid + * @param refreshTokenRequest the request containing the refresh token + * @return a response containing a fresh jwt token and a refresh token + */ public ResponseEntity refreshToken(RefreshTokenDto refreshTokenRequest) { User user = userService.findByRefreshToken(refreshTokenRequest.getRefreshToken()).orElseThrow(() -> new TokenRefreshException( "Refresh token is not in database!")); diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java index 321fb182..ffad1748 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java @@ -3,7 +3,6 @@ import lab.en2b.quizapi.auth.jwt.JwtAuthFilter; import lab.en2b.quizapi.commons.user.UserService; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -26,12 +25,9 @@ @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { - @Autowired - public UserService userService; - @Bean - public JwtAuthFilter authenticationJwtTokenFilter() { - return new JwtAuthFilter(); - } + + public final UserService userService; + public final JwtAuthFilter authenticationJwtTokenFilter; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -39,8 +35,8 @@ public PasswordEncoder passwordEncoder() { @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - CorsConfiguration config = new CorsConfiguration(); // Configure CORS settings here + CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); @@ -64,7 +60,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, Authentication .anyRequest().authenticated()) .csrf(AbstractHttpConfigurer::disable) .authenticationManager(authenticationManager) - .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(authenticationJwtTokenFilter, UsernamePasswordAuthenticationFilter.class) .build(); //TODO: add exception handling } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java index 7690b1d2..10460e1a 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/config/UserDetailsImpl.java @@ -31,7 +31,6 @@ public static UserDetailsImpl build(User user) { } return new UserDetailsImpl(user.getId(),user.getUsername() , user.getEmail(), user.getPassword(), authorities); } - @Override public boolean isAccountNonExpired() { return true; @@ -48,6 +47,11 @@ public boolean isCredentialsNonExpired() { public boolean isEnabled() { return true; } + public List getStringRoles() { + return getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + } @Override public boolean equals(Object o) { if (this == o) @@ -57,10 +61,4 @@ public boolean equals(Object o) { UserDetailsImpl user = (UserDetailsImpl) o; return Objects.equals(id, user.id); } - - public List getStringRoles() { - return getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .collect(Collectors.toList()); - } } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java index 4af19ee2..e87000b7 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java @@ -1,20 +1,18 @@ package lab.en2b.quizapi.auth.dtos; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter +@AllArgsConstructor +@NoArgsConstructor public class RefreshTokenResponseDto { private String token; @JsonProperty("refresh_token") private String refreshToken; - - public RefreshTokenResponseDto(String accessToken, String refreshToken) { - this.token = accessToken; - this.refreshToken = refreshToken; - } - } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java index 9f056081..66bae95c 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtAuthFilter.java @@ -5,7 +5,7 @@ import jakarta.servlet.http.HttpServletResponse; import lab.en2b.quizapi.commons.user.UserService; import lombok.NonNull; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; @@ -16,28 +16,29 @@ import java.io.IOException; @Component +@RequiredArgsConstructor public class JwtAuthFilter extends OncePerRequestFilter { - @Autowired - private JwtUtils jwtUtils; - - @Autowired - private UserService userDetailsService; + private final JwtUtils jwtUtils; + private final UserService userDetailsService; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { String token = parseJwt(request); String email = null; + if(token != null){ email = jwtUtils.getSubjectFromJwtToken(token); } if ( email != null && SecurityContextHolder.getContext().getAuthentication() == null && isValidJwt(token)) { UserDetails userDetails = userDetailsService.loadUserByUsername(email); - // this invokes UsernamePasswordAuthenticationToken, although it uses email as subject not username - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, - null, userDetails.getAuthorities()); + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java index 432b0813..c24f16a4 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/jwt/JwtUtils.java @@ -24,7 +24,6 @@ public class JwtUtils { @Value("${JWT_EXPIRATION_MS}") private Long JWT_EXPIRATION_MS; - public String generateJwtTokenUserPassword(Authentication authentication) { UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); @@ -52,13 +51,6 @@ public boolean validateJwtToken(String authToken) { } return false; } - private Claims extractAllClaims(String token){ - return Jwts.parser() - .verifyWith(getSignInKey()) - .build() - .parseSignedClaims(token) - .getPayload(); - } public T extractClaim(String token, Function claimsResolver){ final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); @@ -70,11 +62,6 @@ public String getSubjectFromJwtToken(String token) { throw new IllegalArgumentException(); } } - private SecretKey getSignInKey(){ - byte[] keyBytes = Decoders.BASE64.decode(JWT_SECRET); - return Keys.hmacShaKeyFor(keyBytes); - } - public String generateTokenFromEmail(String email) { return Jwts.builder() .subject(email) @@ -83,4 +70,15 @@ public String generateTokenFromEmail(String email) { .signWith(getSignInKey()) .compact(); } + private SecretKey getSignInKey(){ + byte[] keyBytes = Decoders.BASE64.decode(JWT_SECRET); + return Keys.hmacShaKeyFor(keyBytes); + } + private Claims extractAllClaims(String token){ + return Jwts.parser() + .verifyWith(getSignInKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } } diff --git a/api/src/test/java/lab/en2b/quizapi/QuizApiApplicationTests.java b/api/src/test/java/lab/en2b/quizapi/QuizApiApplicationTests.java index d9ab6a7b..42b6c339 100644 --- a/api/src/test/java/lab/en2b/quizapi/QuizApiApplicationTests.java +++ b/api/src/test/java/lab/en2b/quizapi/QuizApiApplicationTests.java @@ -6,8 +6,4 @@ @SpringBootTest class QuizApiApplicationTests { - @Test - void contextLoads() { - } - } diff --git a/docker-compose.yml b/docker-compose.yml index e61a32ad..28749098 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,109 +1,37 @@ version: '3' services: - mongodb: - container_name: mongodb-${teamname:-defaultASW} - image: mongo - profiles: ["dev", "prod"] - volumes: - - mongodb_data:/data/db - ports: - - "27017:27017" - networks: - - mynetwork - - authservice: - container_name: authservice-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/authservice:latest - profiles: ["dev", "prod"] - build: ./users/authservice - depends_on: - - mongodb - ports: - - "8002:8002" - networks: - - mynetwork + postgresql: + container_name: postgresql-${teamname:-defaultASW} environment: - MONGODB_URI: mongodb://mongodb:27017/userdb - - userservice: - container_name: userservice-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/userservice:latest - profiles: ["dev", "prod"] - build: ./users/userservice - depends_on: - - mongodb - ports: - - "8001:8001" - networks: - - mynetwork - environment: - MONGODB_URI: mongodb://mongodb:27017/userdb - - gatewayservice: - container_name: gatewayservice-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/gatewayservice:latest - profiles: ["dev", "prod"] - build: ./gatewayservice - depends_on: - - mongodb - - userservice - - authservice - ports: - - "8000:8000" - networks: - - mynetwork - environment: - AUTH_SERVICE_URL: http://authservice:8002 - USER_SERVICE_URL: http://userservice:8001 - - webapp: - container_name: webapp-${teamname:-defaultASW} - image: ghcr.io/arquisoft/wiq_en2b/webapp:latest + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + image: postgres:latest profiles: ["dev", "prod"] - build: ./webapp - depends_on: - - gatewayservice - ports: - - "3000:3000" - - prometheus: - image: prom/prometheus - container_name: prometheus-${teamname:-defaultASW} - profiles: ["dev"] networks: - mynetwork - volumes: - - ./gatewayservice/monitoring/prometheus:/etc/prometheus - - prometheus_data:/prometheus ports: - - "9090:9090" - depends_on: - - gatewayservice - - grafana: - image: grafana/grafana - container_name: grafana-${teamname:-defaultASW} - profiles: ["dev"] + - "5432:5432" + + api: + container_name: api-${teamname:-defaultASW} + image: api:latest + profiles: [ "dev", "prod" ] + build: + context: ./api + args: + DATABASE_USER: ${DATABASE_USER} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + JWT_SECRET: ${JWT_SECRET} networks: - mynetwork - volumes: - - grafana_data:/var/lib/grafana - - ./gatewayservice/monitoring/grafana/provisioning:/etc/grafana/provisioning - environment: - - GF_SERVER_HTTP_PORT=9091 - - GF_AUTH_DISABLE_LOGIN_FORM=true - - GF_AUTH_ANONYMOUS_ENABLED=true - - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin ports: - - "9091:9091" - depends_on: - - prometheus + - "8080:8080" volumes: - mongodb_data: - prometheus_data: - grafana_data: + postgres_data: networks: mynetwork: