Skip to content

Commit

Permalink
Websockets (#14)
Browse files Browse the repository at this point in the history
* .gitignore and additional parameter

* experiment with angular

* Native HTTPS

* https keystore

* Update parameters

* Added self-signed certificates + docker-compose for image usage.

* Update documentation

* Update documentation

* image-based docker-compose for deployment

* Refactoring

* move docker-compose

* Refactoring

* Initial RSocket connection!

* Client Rsocket with composite metadata!

* Remove legacy event system

* Begin RSocket Security!

* Successfully authenticated user via JWT & RSocket

* Removed LinkedHashSet as order-keeping collection for tickets. Sorting of tickets will be done in the client.
Introduced interface "ClassroomDependent" for objects logically depending on an instance of DigitalClassroom.

* Overhauled exceptions

* Prepare event system

* Rebuild downstream API.
Clearer separation of responsibilites.
Improved reactivity through Mono / Flux + clearer code.

* RSocket experiments.

* Eventsystem over RSocket in place. some refactoring in frontend.

* JWT refactoring

* removed unnecessary dependencies.

* cleanup

* Fixed api "/create" idempotency.

* Updated notes.

* published functions.

* Minor changes

* Removed internalMeetingId.
Simplified model classes.

* Backend Classroom API -> RSocket.
Ticket init implemented.
SetOnceProperty for "lateinit val" workaround.
Tickets have ticketId from classroom exclusive sequence.

* RSocketService general purpose interface.

* Minor changes + classroom information.

* cleanup web-gui

* Ticket Service in place and refactoring of web-gui

* UserEvent system + ticket fixes.

* Display username in header.

* beautify

* Detect disconnects!

* minor

* typed UserRole in Client.

* Dynamic websocket endpoint

* simplified ngIfs and removed leave button, since JS cannot close windows it did not spawn.

* Fixed Ticket assignment and double display of current user in user list.

* use map to ensure uniqueness in user list. Map, because we only want to consider userId when checking for equality.

* finalize instead of map to reduce publishing

* Prepare for conference handling. Refactoring.

* Implemented conference system.

* Conference joining works.

* [CI/CD][Docker] added Caching & push image to ghcr.io (#18)

* Invitations - not functional!

* Fixed invitation sending in caller.

* Fixed invitations.

* .gitignore and additional parameter

* experiment with angular

* Update parameters

* Added self-signed certificates + docker-compose for image usage.

* image-based docker-compose for deployment

* move docker-compose

* Refactoring

* Initial RSocket connection!

* Client Rsocket with composite metadata!

* Remove legacy event system

* Begin RSocket Security!

* Successfully authenticated user via JWT & RSocket

* Removed LinkedHashSet as order-keeping collection for tickets. Sorting of tickets will be done in the client.
Introduced interface "ClassroomDependent" for objects logically depending on an instance of DigitalClassroom.

* Overhauled exceptions

* Prepare event system

* Rebuild downstream API.
Clearer separation of responsibilites.
Improved reactivity through Mono / Flux + clearer code.

* RSocket experiments.

* Eventsystem over RSocket in place. some refactoring in frontend.

* JWT refactoring

* removed unnecessary dependencies.

* cleanup

* Fixed api "/create" idempotency.

* Updated notes.

* published functions.

* Minor changes

* Removed internalMeetingId.
Simplified model classes.

* Backend Classroom API -> RSocket.
Ticket init implemented.
SetOnceProperty for "lateinit val" workaround.
Tickets have ticketId from classroom exclusive sequence.

* RSocketService general purpose interface.

* Minor changes + classroom information.

* cleanup web-gui

* Ticket Service in place and refactoring of web-gui

* UserEvent system + ticket fixes.

* Display username in header.

* beautify

* Detect disconnects!

* minor

* typed UserRole in Client.

* Dynamic websocket endpoint

* simplified ngIfs and removed leave button, since JS cannot close windows it did not spawn.

* Fixed Ticket assignment and double display of current user in user list.

* use map to ensure uniqueness in user list. Map, because we only want to consider userId when checking for equality.

* finalize instead of map to reduce publishing

* Prepare for conference handling. Refactoring.

* Implemented conference system.

* Conference joining works.

* Invitations - not functional!

* Fixed invitation sending in caller.

* Fixed invitations.

Co-authored-by: Max Stephan <[email protected]>
  • Loading branch information
snicki13 and mxsph authored Sep 3, 2021
1 parent 6530566 commit 39937e9
Show file tree
Hide file tree
Showing 120 changed files with 2,481 additions and 1,901 deletions.
12 changes: 12 additions & 0 deletions DEVELOPER-NOTES.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,15 @@
Useful BBB API testing tool: https://mconf.github.io/api-mate/

JWT: https://github.com/raphaelDL/spring-webflux-security-jwt

Used Library: https://connect2id.com/products/nimbus-jose-jwt

RSocket: https://github.com/rsocket/rsocket

RSocket.js: https://github.com/rsocket/rsocket-js

RSocket Spring: https://docs.spring.io/spring-framework/docs/current/reference/pdf/rsocket.pdf

Micrometer: https://www.baeldung.com/micrometer

Flowable -> RxJS: https://github.com/netifi/netifi-js-client/blob/3ec310824f643d61a8a6386458ff95524f3373a8/src/rx/FlowableAdapter.js#L35-L47
6 changes: 2 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,17 @@ repositories {
}

dependencies {
// implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
implementation("com.google.guava:guava:30.1.1-jre")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.security:spring-security-rsocket")
implementation("io.jsonwebtoken:jjwt-impl:0.11.2")
implementation("io.jsonwebtoken:jjwt-jackson:0.11.2")
implementation("org.springframework.security:spring-security-messaging")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("org.springframework.boot:spring-boot-starter-rsocket")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("org.scala-lang:scala-library:2.13.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("javax.xml.bind:jaxb-api:2.3.1")
implementation("org.hibernate:hibernate-validator:6.0.16.Final")
Expand Down
73 changes: 0 additions & 73 deletions src/main/kotlin/de/thm/mni/ii/classroom/config/WebsocketConfig.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package de.thm.mni.ii.classroom.controller

import de.thm.mni.ii.classroom.downstream.model.CreateRoomBBB
import de.thm.mni.ii.classroom.downstream.model.JoinRoomBBBResponse
import de.thm.mni.ii.classroom.downstream.model.MessageBBB
import de.thm.mni.ii.classroom.downstream.model.ReturnCodeBBB
import de.thm.mni.ii.classroom.exception.ApiException
import de.thm.mni.ii.classroom.model.api.CreateRoomBBB
import de.thm.mni.ii.classroom.model.api.JoinRoomBBBResponse
import de.thm.mni.ii.classroom.model.api.MessageBBB
import de.thm.mni.ii.classroom.model.api.ReturnCodeBBB
import de.thm.mni.ii.classroom.exception.api.ApiException
import de.thm.mni.ii.classroom.services.DownstreamApiService
import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity
Expand Down Expand Up @@ -33,7 +33,7 @@ class BBBApiController(private val downStreamApiService: DownstreamApiService) {
* @return Mono producing a BBB-like answer in XML format containing an error or information about the classroom.
* @see CreateRoomBBB
*/
@GetMapping("/create")
@GetMapping("/create", produces = [MimeTypeUtils.APPLICATION_XML_VALUE])
fun createClassroomInstance(@RequestParam params: MultiValueMap<String, String>): Mono<CreateRoomBBB>
= downStreamApiService.createClassroom(params)

Expand All @@ -44,7 +44,7 @@ class BBBApiController(private val downStreamApiService: DownstreamApiService) {
* @return Mono producing a BBB-like answer in XML format containing an error or the url to join the classroom.
* @see JoinRoomBBBResponse
*/
@GetMapping("/join")
@GetMapping("/join", produces = [MimeTypeUtils.APPLICATION_XML_VALUE])
fun joinUserToClassroom(@RequestParam params: MultiValueMap<String, String>): Mono<JoinRoomBBBResponse>
= downStreamApiService.joinClassroom(params)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
package de.thm.mni.ii.classroom.controller

import de.thm.mni.ii.classroom.model.Ticket
import de.thm.mni.ii.classroom.security.classroom.ClassroomAuthentication
import de.thm.mni.ii.classroom.services.ClassroomUserService
import de.thm.mni.ii.classroom.security.jwt.ClassroomAuthentication
import org.slf4j.LoggerFactory
import org.springframework.security.access.annotation.Secured
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono

@RestController
@RequestMapping("/classroom-api")
@CrossOrigin
class ClassroomApiController(private val classroomUserService: ClassroomUserService) {
class ClassroomApiController {

val logger = LoggerFactory.getLogger(this::class.java)
private val logger = LoggerFactory.getLogger(this::class.java)

/**
* Does not return any value. This route is called with sessionToken, which is exchanged to a JWT
Expand All @@ -25,23 +22,4 @@ class ClassroomApiController(private val classroomUserService: ClassroomUserServ
logger.info("${auth.principal.fullName} joined classroom ${auth.principal.classroomId}.")
}

@GetMapping("/ticket")
fun getTickets(auth: ClassroomAuthentication) = classroomUserService.getTickets(auth)

@PostMapping("/ticket")
fun createTicket(auth: ClassroomAuthentication, @RequestBody ticket: Ticket) =
classroomUserService.createTicket(auth, ticket)

@PutMapping("/ticket")
@Secured("TUTOR", "TEACHER")
fun assignTicket(auth: ClassroomAuthentication, @RequestBody ticket: Ticket) =
classroomUserService.assignTicket(auth, ticket)

@PostMapping("/ticket/delete")
fun deleteTicket(auth: ClassroomAuthentication, @RequestBody ticket: Ticket) =
classroomUserService.deleteTicket(auth, ticket)

@GetMapping("/users")
fun getUsers(auth: ClassroomAuthentication) = classroomUserService.getUsers(auth)

}
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
package de.thm.mni.ii.classroom.controller

import de.thm.mni.ii.classroom.model.Conference
import de.thm.mni.ii.classroom.model.User
import de.thm.mni.ii.classroom.security.classroom.ClassroomAuthentication
import de.thm.mni.ii.classroom.event.InvitationEvent
import de.thm.mni.ii.classroom.model.classroom.ConferenceInfo
import de.thm.mni.ii.classroom.model.classroom.JoinLink
import de.thm.mni.ii.classroom.model.classroom.User
import de.thm.mni.ii.classroom.services.ConferenceService
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Flux
import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.messaging.handler.annotation.Payload
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.stereotype.Controller
import reactor.core.publisher.Mono

@RestController
@RequestMapping("/classroom-api")
@Controller
class ConferenceController(private val conferenceService: ConferenceService) {

@GetMapping("/conference")
fun getConferences(auth: ClassroomAuthentication): Flux<Conference> {
return conferenceService.getConferencesOfClassroom(auth)
@MessageMapping("socket/conference/create")
fun createConference(@AuthenticationPrincipal user: User, @Payload conferenceInfo: ConferenceInfo): Mono<ConferenceInfo> {
return conferenceService.createConference(user, conferenceInfo)
}

@GetMapping("/conference/user")
fun getUsersInConference(auth: ClassroomAuthentication): Flux<User> {
return conferenceService.getUsersInConferences(auth)
@MessageMapping("socket/conference/join")
fun joinConference(@AuthenticationPrincipal user: User, @Payload conferenceInfo: ConferenceInfo): Mono<JoinLink> {
return conferenceService.joinConference(user, conferenceInfo)
}

@GetMapping("/conference/create")
fun createConference(auth: ClassroomAuthentication): Mono<Conference> {
return conferenceService.createConference(auth)
@MessageMapping("socket/conference/join-user")
fun joinConferenceOfUser(@AuthenticationPrincipal joiningUser: User, @Payload conferencingUser: User): Mono<JoinLink> {
return conferenceService.joinConferenceOfUser(joiningUser, conferencingUser)
}

@PostMapping("/conference/join")
fun joinConference(auth: ClassroomAuthentication, @RequestBody conference: Conference): Mono<String> {
return conferenceService.joinUser(auth, conference)
@MessageMapping("socket/conference/end")
fun endConference(@AuthenticationPrincipal user: User, @Payload conferenceInfo: ConferenceInfo): Mono<Void> {
TODO("NOT YET IMPLEMENTED")
}

@PostMapping("/conference/join/user")
fun joinConferenceOfUser(auth: ClassroomAuthentication, @RequestBody user: User): Mono<String> {
return conferenceService.joinConferenceOfUser(auth, user)
@MessageMapping("socket/conference/invite")
fun inviteToConference(@AuthenticationPrincipal user: User, @Payload invitationEvent: InvitationEvent): Mono<Void> {
return conferenceService.forwardInvitation(user, invitationEvent)
}

}
}
Original file line number Diff line number Diff line change
@@ -1,51 +1,58 @@
package de.thm.mni.ii.classroom.controller

import de.thm.mni.ii.classroom.event.ClassroomEvent
import de.thm.mni.ii.classroom.model.classroom.*
import de.thm.mni.ii.classroom.services.ClassroomEventReceiverService
import de.thm.mni.ii.classroom.services.ClassroomUserService
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.messaging.handler.annotation.Payload
import org.springframework.messaging.rsocket.RSocketRequester
import org.springframework.messaging.rsocket.annotation.ConnectMapping
import reactor.core.publisher.SignalType
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.stereotype.Controller
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono

@Controller
class UserWebSocketController(
private val userService: ClassroomUserService,
private val classroomEventReceiverService: ClassroomEventReceiverService
) {
private val logger: Logger = LoggerFactory.getLogger(this::class.java)

class UserWebSocketController {
@ConnectMapping
fun connect(@AuthenticationPrincipal user: User, requester: RSocketRequester): Mono<Void> {
return userService.userConnected(user, requester)
}

private val logger: Logger = LoggerFactory.getLogger(this::class.java)
@MessageMapping("socket/classroom-event")
fun receiveEvent(@AuthenticationPrincipal user: User, @Payload event: ClassroomEvent) {
classroomEventReceiverService.classroomEventReceived(user, event)
}

@MessageMapping("socket/init-classroom")
fun initClassroom(@AuthenticationPrincipal user: User): Mono<ClassroomInfo> {
return userService.getClassroomInfo(user)
}

private val clients: MutableList<RSocketRequester> = ArrayList()


@ConnectMapping("/classroom")
fun connectClient(requester: RSocketRequester, @Payload client: String) {
requester.rsocket()!!
.onClose()
.doFirst {

// Add all new clients to a client list
logger.info("Client: {} CONNECTED.", client)
clients.add(requester)
}
.doOnError { error: Throwable? ->
// Warn when channels are closed by clients
logger.warn("Channel to client {} CLOSED", client)
}
.doFinally { consumer: SignalType? ->
// Remove disconnected clients from the client list
clients.remove(requester)
logger.info("Client {} DISCONNECTED", client)
}
.subscribe()

// Callback to client, confirming connection
requester.route("client-status")
.data("OPEN")
.retrieveFlux(String::class.java)
.doOnNext { s: String? ->
logger.info(
"Client: {} Free Memory: {}.",
client,
s
)
}
.subscribe()
@MessageMapping("socket/init-tickets")
fun initTickets(@AuthenticationPrincipal user: User): Flux<Ticket> {
logger.info("Ticket init!")
return userService.getTickets(user)
}
}

@MessageMapping("socket/init-users")
fun initUsers(@AuthenticationPrincipal user: User): Flux<UserDisplay> {
return userService.getUserDisplays(user).doOnNext {
logger.info("${it.fullName}, ${it.userId}")
}
}

@MessageMapping("socket/init-conferences")
fun initConferences(@AuthenticationPrincipal user: User): Flux<ConferenceInfo> {
return Flux.empty()
}

}
19 changes: 19 additions & 0 deletions src/main/kotlin/de/thm/mni/ii/classroom/event/ClassroomEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package de.thm.mni.ii.classroom.event

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import java.io.Serializable

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "eventName")
@JsonSubTypes(
JsonSubTypes.Type(value = MessageEvent::class, name = "MessageEvent"),
JsonSubTypes.Type(value = UserEvent::class, name = "UserEvent"),
JsonSubTypes.Type(value = TicketEvent::class, name = "TicketEvent"),
JsonSubTypes.Type(value = ConferenceEvent::class, name = "ConferenceEvent"),
JsonSubTypes.Type(value = InvitationEvent::class, name = "InvitationEvent"),
)
abstract class ClassroomEvent(@field:SuppressWarnings("unused") private val eventName: String): Serializable

data class MessageEvent(val message: String): ClassroomEvent(MessageEvent::class.simpleName!!)
17 changes: 0 additions & 17 deletions src/main/kotlin/de/thm/mni/ii/classroom/event/ClassroomEvents.kt

This file was deleted.

Loading

0 comments on commit 39937e9

Please sign in to comment.