Przeglądaj źródła

прикрутил вебсокеты

kpmy 5 lat temu
rodzic
commit
904ef63b15

+ 6 - 0
pom.xml

@@ -36,6 +36,12 @@
             <version>${spring.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+            <version>${spring.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-undertow</artifactId>

+ 3 - 1
src/app/package.json

@@ -2,7 +2,8 @@
   "name": "frei-app",
   "version": "0.0.1",
   "scripts": {
-    "start": "npx ng serve --host=0.0.0.0 --disable-host-check --aot --source-map --proxy-config proxyconf.json --base-href / --serve-path /",
+    "start": "npx ng serve --aot --source-map --proxy-config proxyconf.json --base-href / --serve-path /",
+    "start-public": "npx ng serve --host=0.0.0.0 --disable-host-check --aot --source-map --proxy-config proxyconf.json --base-href / --serve-path /",
     "build": "npx ng build --prod --base-href /"
   },
   "private": true,
@@ -19,6 +20,7 @@
     "@angular/platform-browser-dynamic": "~9.1.4",
     "@angular/router": "~9.1.4",
     "@auth0/angular-jwt": "^4.0.0",
+    "@stomp/ng2-stompjs": "^7.2.0",
     "moment": "^2.25.3",
     "ngx-init": "^0.1.1",
     "rxjs": "~6.5.4",

+ 4 - 0
src/app/proxyconf.json

@@ -4,5 +4,9 @@
     "pathRewrite": {
       "^/api": ""
     }
+  },
+  "/ws": {
+    "target": "ws://127.0.0.1:4200",
+    "ws": true
   }
 }

+ 2 - 1
src/app/src/app/app.login.component.ts

@@ -4,6 +4,7 @@ import {ReplaySubject} from "rxjs";
 import {MatSnackBar} from "@angular/material/snack-bar";
 import {PersonService} from "./person/person.service";
 import {Router} from "@angular/router";
+import {RxStompService} from "@stomp/ng2-stompjs";
 
 @Component({
   selector: 'app-login',
@@ -15,7 +16,7 @@ export class AppLoginComponent implements OnInit {
   public code: string
   public valid: number = null;
 
-  constructor(private personService: PersonService, private router: Router, private httpClient: HttpClient, private snackBar: MatSnackBar) {
+  constructor(private personService: PersonService, private router: Router, private httpClient: HttpClient, private snackBar: MatSnackBar, private rxStompService: RxStompService) {
     this.qr = new ReplaySubject<QrResponse>();
   }
 

+ 2 - 1
src/app/src/app/app.module.ts

@@ -36,6 +36,7 @@ import {MatCheckboxModule} from "@angular/material/checkbox";
 import {DefaultErrorHandler} from "./misc/DefaultErrorHandler";
 import {PendingIndicator, PendingSnackBar} from "./misc/PendingIndicator";
 import {OrgService} from "./org/org.service";
+import {RxStompService} from "@stomp/ng2-stompjs";
 
 registerLocaleData(localeRu);
 
@@ -95,7 +96,7 @@ export function jwtTokenGetter() {
       useClass: PendingIndicator,
       multi: true
     },
-
+    RxStompService
   ],
   bootstrap: [AppComponent]
 })

+ 21 - 7
src/app/src/app/person/person.page.component.ts

@@ -1,4 +1,4 @@
-import {Component, Inject, OnInit} from "@angular/core";
+import {Component, Inject, OnDestroy, OnInit} from "@angular/core";
 import {AccountEvent, AccountInfo, AccountService} from "../bank/account/account.service";
 import {PersonService} from "./person.service";
 import {HttpClient} from "@angular/common/http";
@@ -7,24 +7,38 @@ import {BankService, DemurrageInfo} from "../bank/bank.service";
 import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
 import {AppComponent} from "../app.component";
 import {OrgInfo, OrgService} from "../org/org.service";
+import {RxStompService} from "@stomp/ng2-stompjs";
+import {Subscription} from "rxjs";
 
 @Component({
   selector: 'person-page',
   templateUrl: 'person.page.component.html',
   styleUrls: ['person.page.component.scss']
 })
-export class PersonPageComponent implements OnInit {
+export class PersonPageComponent implements OnInit, OnDestroy {
 
   accounts: AccountInfo[] = []
   events: AccountEvent[] = []
   orgs: OrgInfo[] = []
   demurrage: DemurrageInfo
+  eventBus: Subscription
 
-  constructor(private app: AppComponent, private orgService: OrgService, private personService: PersonService, private accountService: AccountService, private bankService: BankService, private httpClient: HttpClient, private matDialog: MatDialog) {
+  constructor(private app: AppComponent, private orgService: OrgService, private personService: PersonService, private accountService: AccountService, private bankService: BankService, private httpClient: HttpClient, private matDialog: MatDialog, private rxStompService: RxStompService) {
     app.setTitle("ЛИЧНЫЙ КАБИНЕТ")
   }
 
+  ngOnDestroy(): void {
+    this.eventBus.unsubscribe()
+  }
+
   ngOnInit(): void {
+    this.eventBus = this.rxStompService.watch("/bus/exchange").subscribe(msg => {
+
+    })
+    this.updateNow()
+  }
+
+  updateNow(): void {
     this.personService.getCurrentPerson().subscribe(person => {
       this.orgService.getOrgsInfo(person.auth.id).subscribe(orgs => this.orgs = orgs);
       this.accountService.getAccountsInfo(person.auth.id).subscribe(accounts => {
@@ -49,11 +63,11 @@ export class PersonPageComponent implements OnInit {
 
 
   addAccount() {
-    this.personService.getCurrentPerson().subscribe(person => this.accountService.addAccount(person.auth.id).subscribe(res => this.ngOnInit()))
+    this.personService.getCurrentPerson().subscribe(person => this.accountService.addAccount(person.auth.id).subscribe(res => this.updateNow()))
   }
 
   iNeedMoney() {
-    this.personService.getCurrentPerson().subscribe(person => this.httpClient.post(`/api/old/person/${person.auth.id}/givemoney`, {}).subscribe(res => this.ngOnInit()))
+    this.personService.getCurrentPerson().subscribe(person => this.httpClient.post(`/api/old/person/${person.auth.id}/givemoney`, {}).subscribe(res => this.updateNow()))
   }
 
   getTotatOverall(): number {
@@ -61,7 +75,7 @@ export class PersonPageComponent implements OnInit {
   }
 
   addPrivate() {
-    this.personService.getCurrentPerson().subscribe(person => this.orgService.addPrivateToPerson(person.auth.id).subscribe(res => this.ngOnInit()))
+    this.personService.getCurrentPerson().subscribe(person => this.orgService.addPrivateToPerson(person.auth.id).subscribe(res => this.updateNow()))
   }
 
   joinPrivate() {
@@ -98,7 +112,7 @@ export class PersonPageComponent implements OnInit {
         if (res.action == 'create') {
           let data = <PersonCreditData>res.data
           this.httpClient.post(`/api/old/person/${person.auth.id}/account/${data.account.id}/credit/by-link/${data.code}`, {code: res.key, amount: data.amount}).subscribe((res: any) => {
-            this.ngOnInit()
+            this.updateNow()
             console.log(`transaction done ${res.id}`)
           })
         }

+ 31 - 8
src/app/src/app/person/person.service.ts

@@ -5,6 +5,8 @@ import {JwtHelperService} from "@auth0/angular-jwt";
 import * as _ from 'underscore';
 import {ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot, UrlTree} from "@angular/router";
 import {map} from "rxjs/operators";
+import {InjectableRxStompConfig, RxStompService} from "@stomp/ng2-stompjs";
+import {myRxStompConfig} from "../stomp.config";
 
 @Injectable({providedIn: "root"})
 export class PersonService implements CanActivate, CanActivateChild {
@@ -13,10 +15,14 @@ export class PersonService implements CanActivate, CanActivateChild {
   private helper = new JwtHelperService();
   private currentPerson: Observable<Person>
 
-  constructor() {
+  constructor(private rxStompService: RxStompService) {
     if (!(this.siteId = localStorage.siteId)) {
       localStorage.siteId = this.siteId = uuidv4()
     }
+    let tk = this.getCurrentToken()
+    if (tk != null) {
+      this.startStomp(tk.rawToken);
+    }
   }
 
   canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
@@ -32,9 +38,30 @@ export class PersonService implements CanActivate, CanActivateChild {
     this.currentPerson = null;
   }
 
+  startStomp(token: string) {
+    const config: InjectableRxStompConfig = {...myRxStompConfig, connectHeaders: {"passcode": token}};
+    this.rxStompService.configure(config);
+    this.rxStompService.activate();
+  }
+
   setCurrentPerson(token: string) {
     this.resetCurrentPerson();
     localStorage.setItem("token", token);
+    this.startStomp(token);
+  }
+
+  getCurrentToken(): any {
+    let tkr = localStorage.getItem("token");
+    if (!_.isEmpty(tkr)) {
+      let decodedToken = this.helper.decodeToken(tkr);
+      decodedToken.rawToken = tkr
+      if (!this.helper.isTokenExpired(tkr)) {
+        return decodedToken
+      } else {
+        this.resetCurrentPerson()
+      }
+    }
+    return null
   }
 
   getCurrentPerson(): Observable<Person> {
@@ -43,13 +70,9 @@ export class PersonService implements CanActivate, CanActivateChild {
       setTimeout(() => {
         let p = new Person();
         p.siteId = this.siteId
-        let tkr = localStorage.getItem("token");
-        if (!_.isEmpty(tkr)) {
-          let decodedToken = this.helper.decodeToken(tkr);
-          //const expirationDate = this.helper.getTokenExpirationDate(tkr);
-          if (!this.helper.isTokenExpired(tkr)) {
-            p.auth = <PersonInfo>{id: decodedToken.sub};
-          }
+        let tk = this.getCurrentToken();
+        if (tk != null) {
+          p.auth = <PersonInfo>{id: tk.sub};
         }
         ret.next(p)
       }, 100)

+ 19 - 0
src/app/src/app/stomp.config.ts

@@ -0,0 +1,19 @@
+import {InjectableRxStompConfig} from '@stomp/ng2-stompjs';
+
+export const myRxStompConfig: InjectableRxStompConfig = {
+  brokerURL: `ws://${window.location.host}:${window.location.port}/ws`,
+
+  connectHeaders: {
+    login: 'web',
+    passcode: 'guest'
+  },
+
+  heartbeatIncoming: 0,
+  heartbeatOutgoing: 20000,
+
+  reconnectDelay: 200,
+
+  debug: (msg: string): void => {
+    console.log(new Date(), msg);
+  }
+};

+ 26 - 0
src/app/yarn.lock

@@ -1052,6 +1052,27 @@
     semver "7.1.3"
     semver-intersect "1.4.0"
 
+"@stomp/ng2-stompjs@^7.2.0":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@stomp/ng2-stompjs/-/ng2-stompjs-7.2.0.tgz#2107b7e3d416dc0c69cce2d39f45e1f17261d7fb"
+  integrity sha512-sQO1a7stOIRlj1DO8W2Gix7bUYiw8eU1OzKBLVN+hOTjjYQt93H3ph9ifHvRQlBLMPR/RZnQ3LgUxJVcrhwodQ==
+  dependencies:
+    "@stomp/rx-stomp" "^0.3.0 >=0.3.0"
+    tslib "^1.9.0"
+
+"@stomp/rx-stomp@^0.3.0 >=0.3.0":
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/@stomp/rx-stomp/-/rx-stomp-0.3.5.tgz#3739f44f3f098ddfaf4f46d1f3b6541e3a1d4224"
+  integrity sha512-oEaMkq2IejzVKFg8EtSTe0xnyhPVWJxuQmnuTQStC3EAPcJgH9eQziJc0BR9tUvm6m962v4LPvyJKlzxOJhFWg==
+  dependencies:
+    "@stomp/stompjs" "^5.1.0 >=5.4.2"
+    angular2-uuid "^1.1.1"
+
+"@stomp/stompjs@^5.1.0 >=5.4.2":
+  version "5.4.4"
+  resolved "https://registry.yarnpkg.com/@stomp/stompjs/-/stompjs-5.4.4.tgz#f51d2edf9a00fac645dde3a494738d96ca17e5aa"
+  integrity sha512-RIzQ7MLRSJLUpTHcje1ZclnHH982amJSKC9bDxGO0wyu5OF9ROuuiLf7TxKxo1zUu7lGEYNedg9SEi87uMWDqg==
+
 "@types/color-name@^1.1.1":
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
@@ -1346,6 +1367,11 @@ alphanum-sort@^1.0.0:
   resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
   integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
 
+angular2-uuid@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/angular2-uuid/-/angular2-uuid-1.1.1.tgz#72f03cd532b7f40032eb1ecfb9f8457384be956e"
+  integrity sha1-cvA81TK39AAy6x7PufhFc4S+lW4=
+
 ansi-colors@4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"

+ 1 - 0
src/main/java/inn/ocsf/bee/freigeld/utils/ZBase32Utils.java

@@ -14,4 +14,5 @@ public class ZBase32Utils {
     public static String decode(String str) {
         return new String(codec.decode(str), StandardCharsets.UTF_8);
     }
+
 }

+ 13 - 1
src/main/kotlin/inn/ocsf/bee/freigeld/core/data/CentralBankQueueLevel.kt

@@ -9,6 +9,7 @@ import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
 import org.slf4j.LoggerFactory
 import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.messaging.simp.SimpMessagingTemplate
 import org.springframework.stereotype.Service
 import org.springframework.transaction.annotation.Transactional
 import java.util.*
@@ -22,6 +23,7 @@ class CentralBankQueueLevel : CentralBankAccountLevel() {
 
     companion object {
         val bankTicketChannelName = Bank::class.java.name
+        val bankExchangeBusName = "/bus/exchange"
     }
 
     @Autowired
@@ -30,6 +32,9 @@ class CentralBankQueueLevel : CentralBankAccountLevel() {
     @Inject
     private lateinit var ticketService: TicketService
 
+    @Inject
+    private lateinit var messaging: SimpMessagingTemplate
+
     private val globalFutureMap = mutableMapOf<UUID, CompletableDeferred<Ticket>>()
 
     private val log = LoggerFactory.getLogger(javaClass)
@@ -40,7 +45,14 @@ class CentralBankQueueLevel : CentralBankAccountLevel() {
         globalFutureMap.computeIfAbsent(e.id) { id ->
             ticketService.offerLast(bankTicketChannelName, e)
             val def = CompletableDeferred<Ticket>()
-            def.invokeOnCompletion { t: Throwable? -> if (t == null) ret.complete(e) else ret.completeExceptionally(t) }
+            def.invokeOnCompletion { t: Throwable? ->
+                if (t == null) {
+                    messaging.convertAndSend(bankExchangeBusName, e.id.toString())
+                    ret.complete(e)
+                } else {
+                    ret.completeExceptionally(t)
+                }
+            }
             def
         }
         return ret

+ 3 - 3
src/main/kotlin/inn/ocsf/bee/freigeld/serve/Security.kt

@@ -79,6 +79,7 @@ class Security : WebSecurityConfigurerAdapter() {
                 .disable()
                 .authorizeRequests()
                 .antMatchers("/api/new/person/**").permitAll()
+                .antMatchers("/ws/**").permitAll()
                 .anyRequest().authenticated()
                 .and().exceptionHandling()
                 .authenticationEntryPoint(jwtAuthEntryPoint)
@@ -106,7 +107,7 @@ class JwtTokenUtil : Serializable {
         val tokenLifetime = 5 * 60 * 60L
     }
 
-    fun getUsernameFromToken(token: String?): String {
+    fun getUsernameFromToken(token: String?): String? {
         return getClaimFromToken(token, Claims::getSubject)
     }
 
@@ -174,8 +175,7 @@ class JwtRequestFilter : OncePerRequestFilter() {
         if (username != null && SecurityContextHolder.getContext().authentication == null) {
             val userDetails = jwtUserDetailsService.loadUserByUsername(username)
             if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
-                val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(
-                        userDetails, null, userDetails.authorities)
+                val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
                 usernamePasswordAuthenticationToken.details = WebAuthenticationDetailsSource().buildDetails(request)
                 SecurityContextHolder.getContext().authentication = usernamePasswordAuthenticationToken
             }

+ 67 - 0
src/main/kotlin/inn/ocsf/bee/freigeld/serve/WebSocket.kt

@@ -0,0 +1,67 @@
+package inn.ocsf.bee.freigeld.serve
+
+import io.jsonwebtoken.ExpiredJwtException
+import org.springframework.context.annotation.Configuration
+import org.springframework.messaging.Message
+import org.springframework.messaging.MessageChannel
+import org.springframework.messaging.simp.config.ChannelRegistration
+import org.springframework.messaging.simp.config.MessageBrokerRegistry
+import org.springframework.messaging.simp.stomp.StompCommand
+import org.springframework.messaging.simp.stomp.StompHeaderAccessor
+import org.springframework.messaging.support.ChannelInterceptor
+import org.springframework.messaging.support.MessageHeaderAccessor
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
+import org.springframework.security.core.Authentication
+import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
+import org.springframework.web.socket.config.annotation.StompEndpointRegistry
+import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer
+import javax.inject.Inject
+
+@Configuration
+@EnableWebSocketMessageBroker
+class WebSocket : WebSocketMessageBrokerConfigurer {
+
+    @Inject
+    private lateinit var jwtTokenUtil: JwtTokenUtil
+
+    @Inject
+    private lateinit var jwtUserDetailsService: JwtUserDetailsService
+
+    override fun configureMessageBroker(registry: MessageBrokerRegistry) {
+        registry.enableSimpleBroker("/bus")
+        registry.setApplicationDestinationPrefixes("/app")
+        registry.setUserDestinationPrefix("/user")
+    }
+
+    override fun registerStompEndpoints(registry: StompEndpointRegistry) {
+        registry.addEndpoint("/ws")
+    }
+
+    override fun configureClientInboundChannel(registration: ChannelRegistration) {
+        registration.interceptors(object : ChannelInterceptor {
+            override fun preSend(message: Message<*>, channel: MessageChannel): Message<*>? {
+                val accessor: StompHeaderAccessor? = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java)
+                if (accessor != null && StompCommand.CONNECT.equals(accessor.command)) {
+                    var usernamePasswordAuthenticationToken: Authentication? = null
+                    val jwtToken = accessor.passcode
+                    var username: String? = null
+                    try {
+                        username = jwtTokenUtil.getUsernameFromToken(jwtToken)
+                    } catch (e: IllegalArgumentException) {
+                        //System.out.println("Unable to get JWT Token")
+                    } catch (e: ExpiredJwtException) {
+                        //System.out.println("JWT Token has expired")
+                    }
+                    if (username != null) {
+                        val userDetails = jwtUserDetailsService.loadUserByUsername(username)
+                        usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
+                    }
+
+                    if (usernamePasswordAuthenticationToken == null) throw IllegalArgumentException("where is my token, bitch?")
+                    accessor.user = usernamePasswordAuthenticationToken
+                }
+                return message
+            }
+        })
+    }
+}