Skip to content

Commit

Permalink
Merge pull request #18 from ADPRO-C11/staging
Browse files Browse the repository at this point in the history
Staging
  • Loading branch information
sdikyarts authored May 26, 2024
2 parents 042f2bb + 6230a3e commit 545a2bf
Show file tree
Hide file tree
Showing 17 changed files with 450 additions and 29 deletions.
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
JWT_SECRET = 8Zz5tw0Ionm3XPZZfN0NOml3z9FMfmpgXwovR9fp6ryDIoGRM8EPHAB6iHsc0fb
SONAR_TOKEN = 48bf52cae1f74b4accfabbe8603371968e194fb5
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- main
- staging

jobs:
build:
Expand Down Expand Up @@ -33,6 +34,7 @@ jobs:
sed -i "s|JDBC_DATABASE_USERNAME=.*|JDBC_DATABASE_USERNAME=${{ secrets.JDBC_DATABASE_USERNAME }}|g" src/main/resources/application.properties
sed -i "s|JDBC_DATABASE_PASSWORD=.*|JDBC_DATABASE_PASSWORD=${{ secrets.JDBC_DATABASE_PASSWORD }}|g" src/main/resources/application.properties
sed -i "s|PRODUCTION=.*|PRODUCTION=${{ secrets.PRODUCTION }}|g" src/main/resources/application.properties
sed -i 's|${JWT_SECRET}|'"${{ secrets.JWT_SECRET }}"'|g' src/main/resources/application-prod.properties
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Build with Gradle
Expand Down Expand Up @@ -106,7 +108,7 @@ jobs:
run: export DOCKER_BUILDKIT=1
- name: Build Docker Image
run: |
docker build --build-arg PRODUCTION=$PRODUCTION --build-arg JDBC_DATABASE_PASSWORD=$JDBC_DATABASE_PASSWORD --build-arg JDBC_DATABASE_URL=$JDBC_DATABASE_URL --build-arg JDBC_DATABASE_USERNAME=$JDBC_DATABASE_USERNAME -t ${{ secrets.REGISTRY_USER }}/${{ secrets.IMAGE_NAME }}:${{ secrets.IMAGE_TAG }} .
docker build --build-arg PRODUCTION=$PRODUCTION --build-arg JDBC_DATABASE_PASSWORD=$JDBC_DATABASE_PASSWORD --build-arg JDBC_DATABASE_URL=$JDBC_DATABASE_URL --build-arg JDBC_DATABASE_USERNAME=$JDBC_DATABASE_USERNAME --build-arg JWT_SECRET=${{ secrets.JWT_SECRET }} -t ${{ secrets.REGISTRY_USER }}/${{ secrets.IMAGE_NAME }}:${{ secrets.IMAGE_TAG }} .
docker push ${{ secrets.REGISTRY_USER }}/${{ secrets.IMAGE_NAME }}:${{ secrets.IMAGE_TAG }}
deploy:
Expand Down
75 changes: 75 additions & 0 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: SonarCloud

on:
push:
branches:
- staging
- main
pull_request:
types: [opened, synchronize, reopened]

jobs:
build:
name: Build, analyze, and test
runs-on: ubuntu-latest

services:
postgres:
image: postgres:latest
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: subscription-admin

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "21"
cache: "gradle"
- name: Cache SonarCloud packages
uses: actions/cache@v3
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Wait for PostgreSQL
run: |
for i in {1..30}; do
if pg_isready -h localhost -p 5432 -U postgres; then
echo "PostgreSQL is up and running"
break
fi
echo "Waiting for PostgreSQL..."
sleep 1
done
if ! pg_isready -h localhost -p 5432 -U postgres; then
echo "PostgreSQL failed to start" && exit 1
fi
- name: Build and analyze
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/subscription-admin
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: postgres
run: |
chmod +x ./gradlew
./gradlew build jacocoTestReport sonar --info
17 changes: 17 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
jacoco
id("org.springframework.boot") version "3.2.5"
id("io.spring.dependency-management") version "1.1.4"
id("org.sonarqube") version "4.4.1.3373"
}

group = "snackscription"
Expand Down Expand Up @@ -41,6 +42,22 @@ dependencies {
testImplementation("io.github.bonigarcia:selenium-jupiter:$seleniumJupiterVersion")
testImplementation("io.github.bonigarcia:webdrivermanager:$webdrivermanagerVersion")
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion")
runtimeOnly("io.micrometer:micrometer-registry-prometheus:1.12.5")
implementation("org.springframework.boot:spring-boot-starter-actuator:3.2.5")
implementation("me.paulschwarz:spring-dotenv:4.0.0")
implementation("io.jsonwebtoken:jjwt-api:0.12.5")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-webflux")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.5")
}

sonar {
properties {
property("sonar.projectKey", "ADPRO-C11_snackscription-subscription-admin")
property("sonar.organization", "adpro-c11")
property("sonar.host.url", "https://sonarcloud.io")
}
}

tasks.register<Test>("unitTest") {
Expand Down
19 changes: 19 additions & 0 deletions monitoring/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: '3.7'

services:
prometheus:
image: prom/prometheus:v2.44.0
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml

grafana:
image: grafana/grafana:9.5.2
container_name: grafana
ports:
- "3000:3000"
restart: unless-stopped
volumes:
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
7 changes: 7 additions & 0 deletions monitoring/grafana/provisioning/datasources/datasources.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
8 changes: 8 additions & 0 deletions monitoring/prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
scrape_configs:
- job_name: 'MyAppMetrics'
metrics_path: '/actuator/prometheus'
scrape_interval: 3s
static_configs:
- targets: ['host.docker.internal:8080']
labels:
application: 'subscription'
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package snackscription.subscriptionadmin.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import snackscription.subscriptionadmin.utils.JWTUtils;

import java.io.IOException;
import java.util.Collections;

@Component
public class JWTAuthFilter extends OncePerRequestFilter {

private final JWTUtils jwtUtils;

public JWTAuthFilter(JWTUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwtToken;

if (authHeader == null || authHeader.isBlank()) {
filterChain.doFilter(request, response);
return;
}

jwtToken = authHeader.substring(7);

if (jwtUtils.isTokenValid(jwtToken)) {
String role = jwtUtils.extractRole(jwtToken);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
null, null, Collections.singletonList(() -> role));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package snackscription.subscriptionadmin.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import snackscription.subscriptionadmin.utils.JWTUtils;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JWTUtils jwtUtils;

public SecurityConfig(JWTUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests.requestMatchers("/admin/**", "/public/**").permitAll()
.anyRequest().authenticated())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new JWTAuthFilter(jwtUtils), UsernamePasswordAuthenticationFilter.class);

return httpSecurity.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import snackscription.subscriptionadmin.dto.AdminDTO;
import snackscription.subscriptionadmin.model.AdminSubscription;
import snackscription.subscriptionadmin.service.AdminService;
import snackscription.subscriptionadmin.utils.JWTUtils;

import java.util.List;
import java.util.Optional;
Expand All @@ -16,25 +17,43 @@
@CrossOrigin(origins = "http://localhost:3000")
public class AdminController {
private final AdminService adminService;
private final JWTUtils jwtUtils;

public AdminController(AdminService adminService) {
public AdminController(AdminService adminService, JWTUtils jwtUtils) {
this.adminService = adminService;
this.jwtUtils = jwtUtils;
}

private void validateToken(String token) throws IllegalAccessException {
String jwt = token.replace("Bearer ", "");
if (!jwtUtils.isTokenValid(jwt)) {
throw new IllegalAccessException("You have no permission.");
}
}

@GetMapping("")
public ResponseEntity<String> home() {
return ResponseEntity.ok("Snackscription - Admin Subscription Management API");
}

@PostMapping("/create")
public CompletableFuture<ResponseEntity<AdminSubscription>> create(@RequestBody AdminDTO adminDTO) {
public CompletableFuture<ResponseEntity<AdminSubscription>> create(@RequestHeader(value="Authorization") String token, @RequestBody AdminDTO adminDTO)
throws IllegalAccessException {
validateToken(token);
return adminService.create(adminDTO).thenApply(ResponseEntity::ok)
.exceptionally(ex -> ResponseEntity.badRequest().build());

}

@GetMapping("/list")
public CompletableFuture<ResponseEntity<List<AdminDTO>>> findAll() {
public CompletableFuture<ResponseEntity<List<AdminDTO>>> findAll(@RequestHeader(value="Authorization") String token) throws IllegalAccessException {
validateToken(token);
return adminService.findAll().thenApply(ResponseEntity::ok);
}

@GetMapping("/{subscriptionId}")
public CompletableFuture<ResponseEntity<AdminDTO>> findById(@PathVariable String subscriptionId) {
public CompletableFuture<ResponseEntity<AdminDTO>> findById(@RequestHeader(value="Authorization") String token, @PathVariable String subscriptionId) throws IllegalAccessException {
validateToken(token);
try {
UUID.fromString(subscriptionId);
} catch (IllegalArgumentException e) {
Expand All @@ -45,23 +64,34 @@ public CompletableFuture<ResponseEntity<AdminDTO>> findById(@PathVariable String
}

@PutMapping("/update")
public CompletableFuture<ResponseEntity<AdminSubscription>> update(@RequestBody AdminDTO adminDTO) {
public CompletableFuture<ResponseEntity<AdminSubscription>> update(@RequestHeader(value="Authorization") String token, @RequestBody AdminDTO adminDTO) throws IllegalAccessException {
validateToken(token);
if (adminDTO.getSubscriptionId() == null) {
return CompletableFuture.completedFuture(ResponseEntity.badRequest().build());
}

return adminService.update(adminDTO).thenApply(ResponseEntity::ok)
CompletableFuture<AdminSubscription> updatedSubscription = adminService.update(adminDTO);
if(updatedSubscription == null) {
return CompletableFuture.completedFuture(ResponseEntity.notFound().build());
}

return updatedSubscription.thenApply(ResponseEntity::ok)
.exceptionally(ex -> ResponseEntity.notFound().build());
}

@DeleteMapping("/{subscriptionId}")
public CompletableFuture<ResponseEntity<String>> delete(@PathVariable String subscriptionId) {
public CompletableFuture<ResponseEntity<String>> delete(@RequestHeader(value="Authorization") String token, @PathVariable String subscriptionId) throws IllegalAccessException{
validateToken(token);
try {
UUID.fromString(subscriptionId);
} catch (IllegalArgumentException e) {
return CompletableFuture.completedFuture(ResponseEntity.badRequest().build());
}
return adminService.delete(subscriptionId).thenApply(deleted -> ResponseEntity.ok("DELETE SUCCESS"))
.exceptionally(ex -> ResponseEntity.notFound().build());
try {
return adminService.delete(subscriptionId).thenApply(deleted -> ResponseEntity.ok("DELETE SUCCESS"))
.exceptionally(ex -> ResponseEntity.notFound().build());
} catch (IllegalArgumentException e) {
return CompletableFuture.completedFuture(ResponseEntity.notFound().build());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class AdminServiceImpl implements AdminService{
@Override
@Async
public CompletableFuture<AdminSubscription> create(AdminDTO adminDTO) {
if (adminDTO == null || adminDTO.getSubscriptionId() == null) {
throw new IllegalArgumentException("AdminDTO cannot be null and must have a valid subscriptionId");
}
AdminSubscription adminSubscription = DTOMapper.convertDTOtoModel(adminDTO);
return CompletableFuture.completedFuture(adminRepository.create(adminSubscription));
}
Expand Down
Loading

0 comments on commit 545a2bf

Please sign in to comment.