Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New vue frontend #2

Closed
wants to merge 16 commits into from
Closed
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
18 changes: 18 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,21 @@ jobs:
- name: Bundle application
working-directory: ./frontend
run: npm build

frontend-v2:
name: Build new frontend (vue) with npm
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: ./frontend-new/package-lock.json
- name: Install dependencies
working-directory: ./frontend-new
run: npm install
- name: Bundle application
working-directory: ./frontend-new
run: npm run build
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ out/
**/.gradle
**/build
**/bin

.DS_STORE
13 changes: 13 additions & 0 deletions .idea/runConfigurations/RSocket_Client_for_user_test1.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions .idea/runConfigurations/RSocket_Client_for_user_test2.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.novatecgmbh.eventsourcing.axon.application.config

import com.novatecgmbh.eventsourcing.axon.application.security.CustomUserAuthenticationConverter
import com.novatecgmbh.eventsourcing.axon.application.security.CustomUserDetailsService
import graphql.language.StringValue
import graphql.schema.*
import java.time.LocalDate
Expand All @@ -10,15 +8,6 @@ import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.graphql.execution.RuntimeWiringConfigurer
import org.springframework.graphql.server.WebGraphQlInterceptor
import org.springframework.graphql.server.WebGraphQlRequest
import org.springframework.graphql.server.WebGraphQlResponse
import org.springframework.graphql.server.WebSocketGraphQlInterceptor
import org.springframework.http.HttpHeaders
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.jwt.JwtDecoders
import reactor.core.publisher.Mono

@Configuration
class GraphQlConfiguration(
Expand Down Expand Up @@ -70,33 +59,4 @@ class GraphQlConfiguration(
}
)
.build()

@Bean
fun interceptor(userDetailsService: CustomUserDetailsService) =
CustomWebSocketGraphQlHandlerInterceptor(userDetailsService, issuer)
}

/**
* Workaround for bug reported here: https://github.com/spring-projects/spring-graphql/issues/342
* Can be removed once the propagation works out-of-the-box
*/
class CustomWebSocketGraphQlHandlerInterceptor(
private val userDetailsService: CustomUserDetailsService,
private val issuer: String
) : WebSocketGraphQlInterceptor {

override fun intercept(
request: WebGraphQlRequest,
chain: WebGraphQlInterceptor.Chain
): Mono<WebGraphQlResponse> {
request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.let { accessToken ->
val authConverter = CustomUserAuthenticationConverter(userDetailsService)
val jwt =
JwtDecoders.fromIssuerLocation<JwtDecoder>(issuer)
.decode(accessToken.replace("Bearer ", ""))
val user = authConverter.convert(jwt)
SecurityContextHolder.getContext().authentication = user
}
return super.intercept(request, chain)
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
package com.novatecgmbh.eventsourcing.axon.application.config

import com.novatecgmbh.eventsourcing.axon.application.security.CustomUserAuthenticationConverter
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.graphql.server.support.AbstractAuthenticationWebSocketInterceptor
import org.springframework.graphql.server.support.BearerTokenAuthenticationExtractor
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.STATELESS
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.jwt.JwtDecoders
import org.springframework.security.web.SecurityFilterChain
import reactor.core.publisher.Mono
import reactor.util.context.Context
import reactor.util.context.ContextView

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
class SecurityConfig(val userDetailsService: UserDetailsService) {
class SecurityConfig(
val userDetailsService: UserDetailsService,
@Value("\${spring.security.oauth2.resourceserver.jwt.issuer-uri}") val issuer: String
) {

@Bean
fun apiFilterChain(http: HttpSecurity): SecurityFilterChain {
Expand All @@ -22,7 +35,7 @@ class SecurityConfig(val userDetailsService: UserDetailsService) {
.headers { it.httpStrictTransportSecurity { it.disable() } }
.csrf { it.disable() }
.authorizeHttpRequests {
it.requestMatchers("/graphql/schema/**", "/graphiql/**")
it.requestMatchers("/wsgraphql", "/graphql/schema/**", "/graphiql/**")
.permitAll()
.anyRequest()
.authenticated()
Expand All @@ -39,4 +52,28 @@ class SecurityConfig(val userDetailsService: UserDetailsService) {
@Bean
fun customUserAuthenticationConverter(): CustomUserAuthenticationConverter =
CustomUserAuthenticationConverter(userDetailsService)

@Bean
fun authenticationInterceptor(): CustomAuthenticationWebSocketInterceptor =
CustomAuthenticationWebSocketInterceptor(userDetailsService, issuer)
}

class CustomAuthenticationWebSocketInterceptor(
private val userDetailsService: UserDetailsService,
private val issuer: String
) : AbstractAuthenticationWebSocketInterceptor(BearerTokenAuthenticationExtractor()) {
override fun authenticate(authentication: Authentication): Mono<Authentication> {
val authConverter = CustomUserAuthenticationConverter(userDetailsService)
val jwt =
JwtDecoders.fromIssuerLocation<JwtDecoder>(issuer)
.decode((authentication.principal as String).replace("Bearer ", ""))
val user = authConverter.convert(jwt)

return Mono.just(user)
}

override fun getContextToWrite(securityContext: SecurityContext): ContextView {
val key = SecurityContext::class.java.name // match SecurityContextThreadLocalAccessor key
return Context.of(key, securityContext)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.novatecgmbh.eventsourcing.axon.company.company.graphql

import com.novatecgmbh.eventsourcing.axon.company.company.api.AllCompaniesQuery
import com.novatecgmbh.eventsourcing.axon.company.company.api.CompaniesQuery
import com.novatecgmbh.eventsourcing.axon.company.company.api.CompanyId
import com.novatecgmbh.eventsourcing.axon.company.company.api.CompanyQuery
import com.novatecgmbh.eventsourcing.axon.company.company.api.CompanyQueryResult
import com.novatecgmbh.eventsourcing.axon.project.participant.api.ParticipantQueryResult
import com.novatecgmbh.eventsourcing.axon.project.project.api.CompanyIdsUserCanCreateProjectsForQuery
import java.util.concurrent.CompletableFuture
import org.axonframework.extensions.kotlin.query
import org.axonframework.extensions.kotlin.queryMany
Expand All @@ -30,6 +31,20 @@ class CompanyQueryController(val queryGateway: QueryGateway) {
queryGateway.let { queryGateway.query(CompanyQuery(participant.companyId)) }

@QueryMapping
fun companies(): CompletableFuture<List<CompanyQueryResult>> =
queryGateway.queryMany(AllCompaniesQuery())
fun companies(
@Argument userAllowedToCreateProjectFor: Boolean
): CompletableFuture<List<CompanyQueryResult>> {
val companies =
if (userAllowedToCreateProjectFor) {
queryGateway.queryMany<CompanyId, CompanyIdsUserCanCreateProjectsForQuery>(
CompanyIdsUserCanCreateProjectsForQuery()
)
} else {
CompletableFuture.completedFuture(emptySet<CompanyId>())
}
.get()
return queryGateway.queryMany<CompanyQueryResult, CompaniesQuery>(
CompaniesQuery(companies.toSet())
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class ParticipantQueryController(val queryGateway: QueryGateway) {

// TODO: Change to batch mapping
@SchemaMapping(typeName = "Task")
fun participant(task: TaskQueryResult): CompletableFuture<ParticipantQueryResult?> =
fun assignee(task: TaskQueryResult): CompletableFuture<ParticipantQueryResult?> =
if (task.participantId != null) {
queryGateway.query(ParticipantQuery(task.participantId!!))
} else CompletableFuture.completedFuture(null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package com.novatecgmbh.eventsourcing.axon.project.project.graphql

import com.novatecgmbh.eventsourcing.axon.application.security.RegisteredUserPrincipal
import com.novatecgmbh.eventsourcing.axon.project.project.api.MyProjectsQuery
import com.novatecgmbh.eventsourcing.axon.project.project.api.ProjectId
import com.novatecgmbh.eventsourcing.axon.project.project.api.ProjectQuery
import com.novatecgmbh.eventsourcing.axon.project.project.api.ProjectQueryResult
import com.novatecgmbh.eventsourcing.axon.project.project.api.*
import java.util.concurrent.CompletableFuture
import org.axonframework.extensions.kotlin.query
import org.axonframework.extensions.kotlin.queryMany
import org.axonframework.extensions.kotlin.queryOptional
import org.axonframework.messaging.responsetypes.ResponseTypes
import org.axonframework.queryhandling.QueryGateway
import org.springframework.graphql.data.method.annotation.Argument
import org.springframework.graphql.data.method.annotation.QueryMapping
import org.springframework.graphql.data.method.annotation.SchemaMapping
import org.springframework.graphql.data.method.annotation.SubscriptionMapping
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.annotation.AuthenticationPrincipal
Expand All @@ -35,8 +34,8 @@ class ProjectQueryController(val queryGateway: QueryGateway) {
MyProjectsQuery((user.principal as RegisteredUserPrincipal).identifier)
)

@SubscriptionMapping("projects")
fun projectsAndUpdates(
@SubscriptionMapping("project")
fun projectUpdates(
@AuthenticationPrincipal user: UsernamePasswordAuthenticationToken
): Flux<ProjectQueryResult> {
val query =
Expand All @@ -46,10 +45,35 @@ class ProjectQueryController(val queryGateway: QueryGateway) {
ResponseTypes.instanceOf(ProjectQueryResult::class.java)
)

return query
.initialResult()
.flatMapMany { Flux.fromIterable(it) }
.concatWith(query.updates())
.doFinally { query.cancel() }
return query.updates().doFinally { query.cancel() }

// Alternative if this function should also return the current projects before the updates
// return queryl
// .initialResult()
// .flatMapMany { Flux.fromIterable(it) }
// .concatWith(query.updates())
// .doFinally { query.cancel() }
}

@SchemaMapping(typeName = "Project")
fun statistics(project: ProjectQueryResult): CompletableFuture<ProjectStatistics> =
queryGateway
.query<ProjectDetailsQueryResult, ProjectDetailsQuery>(
ProjectDetailsQuery(project.identifier)
)
.thenApply { result ->
ProjectStatistics(
tasks =
TaskStatistics(
all = result.allTasksCount.toInt(),
planned = result.plannedTasksCount.toInt(),
started = result.startedTasksCount.toInt(),
completed = result.completedTasksCount.toInt()
)
)
}
}

data class ProjectStatistics(val tasks: TaskStatistics)

data class TaskStatistics(val all: Int, val planned: Int, val started: Int, val completed: Int)
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
package com.novatecgmbh.eventsourcing.axon.project.task.graphql

import com.novatecgmbh.eventsourcing.axon.project.project.api.ProjectId
import com.novatecgmbh.eventsourcing.axon.project.project.api.ProjectQueryResult
import com.novatecgmbh.eventsourcing.axon.project.task.api.TaskId
import com.novatecgmbh.eventsourcing.axon.project.task.api.TaskQuery
import com.novatecgmbh.eventsourcing.axon.project.task.api.TaskQueryResult
import com.novatecgmbh.eventsourcing.axon.project.task.api.TasksByMultipleProjectsQuery
import com.novatecgmbh.eventsourcing.axon.project.task.api.*
import java.time.LocalDate
import java.util.concurrent.CompletableFuture
import org.axonframework.extensions.kotlin.query
import org.axonframework.extensions.kotlin.queryMany
import org.axonframework.messaging.responsetypes.ResponseTypes
import org.axonframework.queryhandling.QueryGateway
import org.springframework.graphql.data.method.annotation.Argument
import org.springframework.graphql.data.method.annotation.QueryMapping
import org.springframework.graphql.data.method.annotation.SchemaMapping
import org.springframework.graphql.data.method.annotation.SubscriptionMapping
import org.springframework.stereotype.Controller
import reactor.core.publisher.Flux

@Controller
class TaskQueryController(val queryGateway: QueryGateway) {
Expand Down Expand Up @@ -43,6 +44,22 @@ class TaskQueryController(val queryGateway: QueryGateway) {
// }
// .toMono()

@QueryMapping
fun tasks(@Argument projectIdentifier: ProjectId): CompletableFuture<List<TaskQueryResult>> =
queryGateway.queryMany(TasksByProjectQuery(projectIdentifier))

@SubscriptionMapping("tasks")
fun taskUpdates(@Argument projectIdentifier: ProjectId): Flux<TaskQueryResult> {
val query =
queryGateway.subscriptionQuery(
TasksByProjectQuery(projectIdentifier),
ResponseTypes.multipleInstancesOf(TaskQueryResult::class.java),
ResponseTypes.instanceOf(TaskQueryResult::class.java)
)

return query.updates().doFinally { query.cancel() }
}

@SchemaMapping(typeName = "Project")
fun tasks(
project: ProjectQueryResult,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.novatecgmbh.eventsourcing.axon.user.user.graphql

import com.novatecgmbh.eventsourcing.axon.application.security.RegisteredUserPrincipal
import com.novatecgmbh.eventsourcing.axon.company.employee.api.EmployeeQueryResult
import com.novatecgmbh.eventsourcing.axon.project.participant.api.ParticipantQueryResult
import com.novatecgmbh.eventsourcing.axon.user.api.AllUsersQuery
Expand All @@ -11,6 +12,8 @@ import org.axonframework.extensions.kotlin.queryMany
import org.axonframework.queryhandling.QueryGateway
import org.springframework.graphql.data.method.annotation.QueryMapping
import org.springframework.graphql.data.method.annotation.SchemaMapping
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.stereotype.Controller

@Controller
Expand All @@ -19,6 +22,12 @@ class UserQueryController(val queryGateway: QueryGateway) {
@QueryMapping
fun users(): CompletableFuture<List<UserQueryResult>> = queryGateway.queryMany(AllUsersQuery())

@QueryMapping
fun currentUser(
@AuthenticationPrincipal user: UsernamePasswordAuthenticationToken
): CompletableFuture<UserQueryResult> =
queryGateway.query(UserQuery((user.principal as RegisteredUserPrincipal).identifier))

// TODO: Change to BatchMapping to be more efficient
@SchemaMapping(typeName = "Participant")
fun user(participant: ParticipantQueryResult): CompletableFuture<UserQueryResult> =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
extend type Query {
company(identifier:ID!): Company
companies: [Company]
companies(userAllowedToCreateProjectFor: Boolean = false): [Company]
employee(identifier:ID!): Employee
}

Expand Down
Loading
Loading