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
7 changes: 6 additions & 1 deletion backend/api_gateway/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,18 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("io.nats:jnats:2.17.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
developmentOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
testImplementation("io.projectreactor:reactor-test")
}

tasks.withType<KotlinCompile> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,38 @@ package com.linkedout.backend.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtGrantedAuthoritiesConverterAdapter
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
internal open class SecurityConfig {
@Bean
@Throws(Exception::class)
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http.oauth2ResourceServer { auth -> auth.jwt { } }
.sessionManagement { session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.csrf { csrf -> csrf.disable() }
.authorizeHttpRequests { auth -> auth.anyRequest().authenticated() }
.authorizeExchange { auth -> auth.anyExchange().hasRole("client_candidate") }

return http.build()
}

@Bean
open fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
open fun jwtAuthenticationConverter(): ReactiveJwtAuthenticationConverter {
val jwtGrantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("platform_roles")
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_")

val jwtAuthenticationConverter = JwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter)
val jwtAuthenticationConverter = ReactiveJwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(ReactiveJwtGrantedAuthoritiesConverterAdapter(jwtGrantedAuthoritiesConverter))
return jwtAuthenticationConverter
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.linkedout.backend.controller

import com.linkedout.backend.model.JobCategory
import com.linkedout.backend.service.JobCategoryService
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux

@RestController
@RequestMapping("/api/v1/jobs/categories")
class JobCategoriesController(private val jobCategoryService: JobCategoryService) {
@GetMapping
open fun getJobCategories(request: ServerHttpRequest): Flux<JobCategory> {
return jobCategoryService.findAll(request.id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,27 @@ package com.linkedout.backend.controller

import com.linkedout.backend.model.Job
import com.linkedout.backend.service.JobService
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono

@RestController
@RequestMapping("/api/v1/jobs")
open class JobsController(private val jobService: JobService) {
@GetMapping
open fun getJobs(): List<Job> {
return jobService.findAll()
open fun getJobs(request: ServerHttpRequest): Flux<Job> {
return jobService.findAll(request.id)
}

@GetMapping("/{id}")
open fun getJob(
@PathVariable id: String,
request: ServerHttpRequest
): Mono<Job> {
return jobService.findOne(request.id, id)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.linkedout.backend.model

data class JobCategory(
val id: String,
val category: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.linkedout.backend.service

import com.linkedout.backend.model.JobCategory
import com.linkedout.common.service.NatsService
import com.linkedout.common.utils.RequestResponseFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux

@Service
class JobCategoryService(private val natsService: NatsService, @Value("\${app.services.jobCategories.subjects.findAll}") private val findAllSubject: String) {
fun findAll(requestId: String): Flux<JobCategory> {
// Request job categories from the jobs service
val response = natsService.requestWithReply(findAllSubject, RequestResponseFactory.newRequest(requestId).build())

// Handle the response
if (!response.hasGetJobCategoriesResponse()) {
throw Exception("Invalid response")
}

val getJobCategoriesResponse = response.getGetJobCategoriesResponse()

return Flux.fromIterable(getJobCategoriesResponse.categoriesList)
.map { jobCategory ->
JobCategory(jobCategory.id, jobCategory.category)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ package com.linkedout.backend.service
import com.linkedout.backend.model.Job
import com.linkedout.common.service.NatsService
import com.linkedout.common.utils.RequestResponseFactory
import com.linkedout.proto.services.Jobs.GetJobRequest
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono

@Service
class JobService(private val natsService: NatsService, @Value("\${app.services.jobs.subjects.findAll}") private val findAllSubject: String) {
fun findAll(): List<Job> {
class JobService(private val natsService: NatsService, @Value("\${app.services.jobs.subjects.findAll}") private val findAllSubject: String, @Value("\${app.services.jobs.subjects.findOne}") private val findOneSubject: String) {
fun findAll(requestId: String): Flux<Job> {
// Request jobs from the jobs service
val response = natsService.requestWithReply(findAllSubject, RequestResponseFactory.newRequest().build())
val response = natsService.requestWithReply(findAllSubject, RequestResponseFactory.newRequest(requestId).build())

// Handle the response
if (!response.hasGetJobsResponse()) {
Expand All @@ -19,9 +22,30 @@ class JobService(private val natsService: NatsService, @Value("\${app.services.j

val getJobsResponse = response.getGetJobsResponse()

return getJobsResponse.jobsList
return Flux.fromIterable(getJobsResponse.jobsList)
.map { job ->
Job(job.id, job.title, job.category)
}
}

fun findOne(requestId: String, id: String): Mono<Job> {
// Request jobs from the jobs service
val request = RequestResponseFactory.newRequest(requestId)
.setGetJobRequest(
GetJobRequest.newBuilder()
.setId(id)
.build()
)
.build()

val response = natsService.requestWithReply(findOneSubject, request)

// Handle the response
if (!response.hasGetJobResponse()) {
throw Exception("Invalid response")
}

val getJobResponse = response.getGetJobResponse()
return Mono.just(Job(getJobResponse.job.id, getJobResponse.job.title, getJobResponse.job.category))
}
}
6 changes: 5 additions & 1 deletion backend/api_gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ spring:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8180/realms/linkedout
issuer-uri: https://auth.linkedout.cluster-2020-5.dopolytech.fr/realms/linkedout-dev

server:
port: 9090
Expand All @@ -18,3 +18,7 @@ app:
jobs:
subjects:
findAll: jobs.findAll
findOne: jobs.findOne
jobCategories:
subjects:
findAll: jobs.findAllCategories
1 change: 1 addition & 0 deletions backend/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ repositories {
dependencies {
implementation(project(":protobuf"))
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework:spring-web")
implementation("org.springframework:spring-messaging")
implementation("io.nats:jnats:2.17.1")
implementation("com.google.protobuf:protobuf-kotlin:3.25.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import io.nats.client.Connection
import io.nats.client.Nats
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Lazy
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatusCode
import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException
import java.time.Duration

@Service
Expand All @@ -17,10 +20,12 @@ class NatsService(@Value("\${nats.spring.server}") private val natsUrl: String,

fun requestWithReply(subject: String, request: Request): Response {
val rawResponse = nc.request(subject, request.toByteArray(), timeout)
?: throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No response from service")

val response = Response.parseFrom(rawResponse.data)

if (!response.success) {
throw Exception(response.errorMessage)
if (response.hasError()) {
throw ResponseStatusException(HttpStatusCode.valueOf(response.error.errorCode), response.error.errorMessage)
}

return response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,26 @@ package com.linkedout.common.utils

import com.linkedout.proto.RequestOuterClass
import com.linkedout.proto.ResponseOuterClass
import java.util.UUID
import org.springframework.http.HttpStatus

class RequestResponseFactory private constructor() {
companion object {
fun newRequest(): RequestOuterClass.Request.Builder {
fun newRequest(requestId: String): RequestOuterClass.Request.Builder {
return RequestOuterClass.Request.newBuilder()
.setRequestId(UUID.randomUUID().toString())
.setRequestId(requestId)
}

fun newSuccessfulResponse(): ResponseOuterClass.Response.Builder {
return ResponseOuterClass.Response.newBuilder()
.setSuccess(true)
}

fun newFailedResponse(message: String): ResponseOuterClass.Response.Builder {
fun newFailedResponse(message: String, errorCode: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR): ResponseOuterClass.Response.Builder {
return ResponseOuterClass.Response.newBuilder()
.setSuccess(false)
.setErrorMessage(message)
.setError(
ResponseOuterClass.Error.newBuilder()
.setErrorMessage(message)
.setErrorCode(errorCode.value())
)
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.linkedout.jobs.function.jobCategories

import com.linkedout.common.utils.RequestResponseFactory
import com.linkedout.jobs.service.JobCategoryService
import com.linkedout.proto.RequestOuterClass.Request
import com.linkedout.proto.ResponseOuterClass.Response
import com.linkedout.proto.models.JobCategoryOuterClass
import com.linkedout.proto.services.Jobs
import org.springframework.stereotype.Component
import java.util.function.Function

@Component
class GetJobCategories(private val jobCategoryService: JobCategoryService) : Function<Request, Response> {
override fun apply(t: Request): Response {
// Get all the job categories from the database
val responseMono = jobCategoryService.findAll()
.map { jobCategory ->
JobCategoryOuterClass.JobCategory.newBuilder()
.setId(jobCategory.id.toString())
.setCategory(jobCategory.title)
.build()
}
.reduce(Jobs.GetJobCategoriesResponse.newBuilder()) { builder, jobCategory ->
builder.addCategories(jobCategory)
builder
}
.map { builder ->
builder.build()
}

// Block until the response is ready
val response = try {
responseMono.block()
} catch (e: Exception) {
return RequestResponseFactory.newFailedResponse(e.message ?: "Unknown error").build()
}
?: return RequestResponseFactory.newSuccessfulResponse()
.setGetJobCategoriesResponse(Jobs.GetJobCategoriesResponse.getDefaultInstance())
.build()

return RequestResponseFactory.newSuccessfulResponse()
.setGetJobCategoriesResponse(response)
.build()
}
}
Loading