Browse Source

глобальный labeling для всего

κρμγ 5 years ago
parent
commit
fd8c271f01

+ 4 - 0
db/2020-06/indexes.js

@@ -0,0 +1,4 @@
+db.getCollection("kv-store").createIndex({"bucket": 1, "keyStr": 1}, {
+    "unique": true,
+    "background": true
+})

+ 1 - 0
src/app/package.json

@@ -26,6 +26,7 @@
     "rxjs": "~6.5.4",
     "tslib": "^1.10.0",
     "underscore": "^1.10.2",
+    "underscore.string": "^3.3.5",
     "uuid": "^8.0.0",
     "zone.js": "~0.10.2"
   },

+ 16 - 12
src/app/src/app/app.login.component.html

@@ -1,13 +1,17 @@
-<div>
-  <p>Всё просто, для регистрации введите проверочную строку <strong>{{(qr | async)?.key}}</strong> или отсканируйте qr-код с помощью приложения Authenticator, затем введите шестизначный код из приложения и можно начинать.</p>
-  <p>Или, если вы уже зарегистрированы, просто введите шестизначный код.</p>
-  <div><img alt="your qr-code here" src="{{(qr | async)?.img}}"></div>
-  <div fxLayout="row" fxLayoutAlign="start center">
-    <mat-form-field>
-      <mat-label>Проверочный код</mat-label>
-      <input (ngModelChange)="checkCode($event)" [ngModel]="code" matInput maxlength="6" minlength="6" pattern="^[0-9]+$" placeholder="123456" type="text"/>
-    </mat-form-field>
-    <mat-icon *ngIf="valid === 1">done</mat-icon>
-    <mat-icon *ngIf="valid === 0">not_interested</mat-icon>
-  </div>
+<div fxLayout="column" fxLayoutAlign="center center">
+  <mat-card style="width: 30em">
+    <p>Всё просто, для регистрации введите проверочную строку <br/><strong>{{(qr | async)?.key}}</strong><br/> или отсканируйте qr-код с помощью приложения Authenticator*, затем введите шестизначный код из приложения и можно начинать.</p>
+    <p>Или, если вы уже зарегистрированы, просто введите шестизначный код в поле ниже.</p>
+    <div fxLayout="row" fxLayoutAlign="center center"><img alt="your qr-code here" height="320" src="{{(qr | async)?.img}}" width="320"></div>
+    <p>Введите проверочный код из приложения Authenticator*</p>
+    <div fxLayout="row" fxLayoutAlign="center center">
+      <mat-form-field>
+        <mat-label>Проверочный код</mat-label>
+        <input (ngModelChange)="checkCode($event)" [ngModel]="code" matInput maxlength="6" minlength="6" pattern="^[0-9]+$" placeholder="123456" type="text"/>
+      </mat-form-field>
+      <mat-icon *ngIf="valid === 1">done</mat-icon>
+      <mat-icon *ngIf="valid === 0">not_interested</mat-icon>
+    </div>
+    <p>*приложение Authenticator может быть заменено аналогичным приложением с функциями TOTP</p>
+  </mat-card>
 </div>

+ 5 - 3
src/app/src/app/app.module.ts

@@ -33,11 +33,13 @@ import {MatSelectModule} from "@angular/material/select";
 import {MatSliderModule} from "@angular/material/slider";
 import {MatRadioModule} from "@angular/material/radio";
 import {MatCheckboxModule} from "@angular/material/checkbox";
-import {DefaultErrorHandler} from "./misc/DefaultErrorHandler";
-import {PendingIndicator, PendingSnackBar} from "./misc/PendingIndicator";
+import {DefaultErrorHandler} from "./misc/default.error.handler";
+import {PendingIndicator, PendingSnackBar} from "./misc/pending.indicator";
 import {OrgService} from "./org/org.service";
 import {RxStompService} from "@stomp/ng2-stompjs";
 import {PrivotalPageComponent} from "./org/privotal.page.component";
+import {LabeledComponent} from "./misc/labeled.component";
+import {ElsePipe} from "./misc/else.pipe";
 
 registerLocaleData(localeRu);
 
@@ -47,7 +49,7 @@ export function jwtTokenGetter() {
 
 @NgModule({
   declarations: [
-    AppComponent, AppLoginComponent, AppIndexComponent, PersonPageComponent, PersonDebitDialogComponent, PendingSnackBar, PersonCreditDialogComponent, PrivotalPageComponent
+    AppComponent, AppLoginComponent, AppIndexComponent, PersonPageComponent, PersonDebitDialogComponent, PendingSnackBar, PersonCreditDialogComponent, PrivotalPageComponent, LabeledComponent, ElsePipe,
   ],
   entryComponents: [
     PersonDebitDialogComponent, PendingSnackBar, PersonCreditDialogComponent

+ 0 - 0
src/app/src/app/misc/DefaultErrorHandler.ts → src/app/src/app/misc/default.error.handler.ts


+ 11 - 0
src/app/src/app/misc/else.pipe.ts

@@ -0,0 +1,11 @@
+import {Pipe, PipeTransform} from "@angular/core";
+
+@Pipe({name: 'else'})
+export class ElsePipe implements PipeTransform {
+  transform(value: any, alternative: string): string {
+    if (value == undefined || value === null || (value + "") == "")
+      return alternative;
+    else
+      return value
+  }
+}

+ 6 - 0
src/app/src/app/misc/labeled.component.html

@@ -0,0 +1,6 @@
+<span (click)="!labelOnly && !!edit()" *ngIf="mode == 'view'" [ngStyle]="{cursor: (!labelOnly ? 'help' : 'inherit')}">{{label | async | else: defaultLabel}}</span>
+<div *ngIf="mode == 'edit'" fxLayout="row" fxLayoutAlign="start center">
+  <input (keydown)="keyPress($event)" [(ngModel)]="labelInput" autofocus matInput size="10"/>
+  <mat-icon (click)="save()" style="cursor: pointer">check</mat-icon>
+  <mat-icon (click)="cancel()" style="cursor: pointer">clear</mat-icon>
+</div>

+ 101 - 0
src/app/src/app/misc/labeled.component.ts

@@ -0,0 +1,101 @@
+import {Component, Injectable, Input, OnInit} from "@angular/core";
+import {Observable, ReplaySubject} from "rxjs";
+import {HttpClient} from "@angular/common/http";
+import {PersonService} from "../person/person.service";
+import * as _ from 'underscore';
+import * as S from 'underscore.string';
+
+@Component({
+  selector: 'label-id',
+  templateUrl: 'labeled.component.html'
+})
+export class LabeledComponent implements OnInit {
+
+  @Input("default")
+  public defaultLabel: string = '???'
+
+  @Input("id")
+  public id: string = null
+
+  @Input("labelOnly")
+  public labelOnly: boolean = false;
+
+  public mode = 'view'
+
+  public labelInput: string
+
+  public label: Observable<String> = null
+
+  constructor(private labeledService: LabeledService) {
+
+  }
+
+  edit() {
+    this.label.subscribe(labelValue => {
+      this.labelInput = labelValue ? labelValue.toString() : this.defaultLabel
+      this.mode = 'edit'
+    })
+  }
+
+  save() {
+    this.labeledService.putLabel(this.id, !_.isEmpty(this.labelInput) ? this.labelInput : null).subscribe(res => {
+      this.ngOnInit();
+      this.mode = 'view'
+    })
+  }
+
+  cancel() {
+    this.mode = 'view'
+  }
+
+  keyPress(e: KeyboardEvent) {
+    if (S(e.code).contains('Enter')) {
+      this.save()
+    } else if (S(e.code).contains('Esc')) {
+      this.cancel()
+    }
+  }
+
+  ngOnInit(): void {
+    let repl = new ReplaySubject<string>()
+    this.labeledService.getLabel(this.id, repl)
+    this.label = repl
+  }
+
+}
+
+@Injectable({providedIn: 'root'})
+export class LabeledService {
+
+  private cache = new Map<string, string>()
+
+  constructor(private httpClient: HttpClient, private personService: PersonService) {
+  }
+
+  putLabel(id: string, label: string): Observable<LabelResp> {
+    let ret = new ReplaySubject<LabelResp>();
+    this.cache.delete(id)
+    this.personService.getCurrentPerson().subscribe(person => this.httpClient.post(`/api/old/person/${person.auth.id}/opts/label/${id}`, {label}).subscribe((res: LabelResp) => ret.next(res)))
+    return ret;
+  }
+
+  getLabel(id: string, repl: ReplaySubject<string>) {
+    if (this.cache.has(id)) {
+      setTimeout(() => {
+        repl.next(this.cache.get(id))
+      }, 10)
+    } else {
+      this.personService.getCurrentPerson().subscribe(person => {
+        this.httpClient.get(`/api/old/person/${person.auth.id}/opts/label/${id}`).subscribe((res: LabelResp) => {
+          repl.next(res?.label)
+          this.cache.set(id, res?.label)
+        })
+      })
+    }
+  }
+}
+
+class LabelResp {
+  labelId: string
+  label: string
+}

+ 0 - 0
src/app/src/app/misc/PendingIndicator.ts → src/app/src/app/misc/pending.indicator.ts


+ 8 - 3
src/app/src/app/person/person.page.component.html

@@ -8,7 +8,9 @@
     <mat-card>
       <mat-card-header>
         <div class="account-header-avatar" mat-card-avatar></div>
-        <mat-card-title>Свободный аккаунт</mat-card-title>
+        <mat-card-title>
+          <label-id default="Аккаунт" id="account-name"></label-id>
+        </mat-card-title>
         <mat-card-subtitle>Пульт управления свободными деньгами!</mat-card-subtitle>
       </mat-card-header>
       <mat-card-content>
@@ -35,7 +37,7 @@
       <mat-list-item (mouseenter)="hovered[idx] = true" (mouseleave)="hovered[idx] = false" *ngFor="let account of accounts; let idx=index">
         <mat-icon mat-list-icon>toll</mat-icon>
         <div fxLayout="row" fxLayoutAlign="space-between center" mat-line>
-          <span>Cчёт: {{account.overall}}<span>₣</span></span>
+          <span><label-id [id]="account.id" default="Счёт"></label-id></span>
           <div fxLayout="row" fxLayoutAlign="start center">
             <button (click)="showDebitDialog(account)" [disabled]="!hovered[idx]" class="mat-elevation-z0" color="warn" mat-button matTooltip="ПОПОЛНИТЬ">
               <mat-icon>archive</mat-icon>
@@ -45,6 +47,7 @@
             </button>
           </div>
         </div>
+        <div mat-line>Баланс: <span>{{account.overall}}<span>₣</span></span></div>
       </mat-list-item>
       <mat-list-item>
         <button (click)="addAccount()" mat-button>ОТКРЫТЬ СЧЁТ</button>
@@ -101,7 +104,9 @@
     <mat-list>
       <mat-list-item *ngFor="let org of orgs" [routerLink]="['/privotal', {privateId: org.id}]" class="href">
         <mat-icon mat-list-icon>business</mat-icon>
-        <div mat-line>Организация</div>
+        <div mat-line>
+          <label-id [id]="org.id" [labelOnly]="true" default="Организация"></label-id>
+        </div>
         <div mat-line>
           <span *ngIf="org.position == 'owner'">вы владелец</span>
           <span *ngIf="org.position == 'coowner'">вы совладелец</span>

+ 14 - 1
src/app/yarn.lock

@@ -6898,6 +6898,11 @@ split-string@^3.0.1, split-string@^3.0.2:
   dependencies:
     extend-shallow "^3.0.0"
 
+sprintf-js@^1.0.3:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
+  integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
+
 sprintf-js@~1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -7421,6 +7426,14 @@ typescript@~3.8.3:
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
   integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
 
+underscore.string@^3.3.5:
+  version "3.3.5"
+  resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.5.tgz#fc2ad255b8bd309e239cbc5816fd23a9b7ea4023"
+  integrity sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==
+  dependencies:
+    sprintf-js "^1.0.3"
+    util-deprecate "^1.0.2"
+
 underscore@^1.10.2:
   version "1.10.2"
   resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.10.2.tgz#73d6aa3668f3188e4adb0f1943bd12cfd7efaaaf"
@@ -7553,7 +7566,7 @@ use@^3.1.0:
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
-util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=

+ 12 - 0
src/main/kotlin/inn/ocsf/bee/freigeld/core/model/PersonalizedId.kt

@@ -0,0 +1,12 @@
+package inn.ocsf.bee.freigeld.core.model
+
+import inn.ocsf.bee.freigeld.utils.KeyValueId
+import java.util.*
+
+data class PersonalizedId(val personId: UUID, val id: Any) : KeyValueId() {
+
+    override fun getKey(): String {
+        return "${personId}/${id}"
+    }
+
+}

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

@@ -1,5 +1,7 @@
 package inn.ocsf.bee.freigeld.serve
 
+import inn.ocsf.bee.freigeld.core.model.GlobalWorld
+import inn.ocsf.bee.freigeld.core.model.Person
 import io.jsonwebtoken.Claims
 import io.jsonwebtoken.ExpiredJwtException
 import io.jsonwebtoken.Jwts
@@ -29,6 +31,7 @@ import org.springframework.security.web.AuthenticationEntryPoint
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
 import org.springframework.stereotype.Component
+import org.springframework.stereotype.Service
 import org.springframework.web.filter.OncePerRequestFilter
 import java.io.Serializable
 import java.nio.charset.StandardCharsets
@@ -222,4 +225,16 @@ class JwtUserDetailsService : UserDetailsService {
 
 }
 
+@Service
+class PersonService {
+
+    @Inject
+    private lateinit var world: GlobalWorld
+
+    fun getCurrentPerson(): Person? {
+        val user = SecurityContextHolder.getContext().authentication.principal as JwtUser
+        return world.getPerson(UUID.fromString(user.username))
+    }
+}
+
 class JwtUser(username: String, password: String, authorities: MutableCollection<out GrantedAuthority>) : User(username, password, authorities)

+ 1 - 3
src/main/kotlin/inn/ocsf/bee/freigeld/serve/rest/PersonController.kt

@@ -56,8 +56,6 @@ class PersonController {
 
     private val log = LoggerFactory.getLogger(javaClass)
 
-
-
     @RequestMapping("/api/old/person/{personId}/get-credit-link/{linkId}", method = [RequestMethod.GET])
     fun getCreditLink(@PathVariable("personId") personId: UUID, @PathVariable("linkId") linkId: String): ResponseEntity<Map<String, Any?>> {
         val link = linkRepo.findById(linkId).filter { it is PublicLinkDebitAccount }.map { it as PublicLinkDebitAccount }.orElse(null)
@@ -118,7 +116,7 @@ class PersonController {
     @Profile("dev")
     @RequestMapping("api/old/person/{personId}/givemoney", method = [RequestMethod.POST])
     fun giveMoney(@PathVariable("personId") personId: UUID): ResponseEntity<Map<String, Any?>> {
-        val account = bank.getAccounts(personId).randomOrNull()
+        val account = bank.getAccounts(personId).sortedBy { it.overall }.firstOrNull()
         if (account != null) {
             bank.exchange(account, bank.getSelfAccount(), (100 * Math.random()).toLong(), BankExchangeDetails.BankExchangeDetailsBuilder.aBankExchangeDetails().withDescription("basic income").build()).get(5, TimeUnit.SECONDS)
         }

+ 43 - 0
src/main/kotlin/inn/ocsf/bee/freigeld/serve/rest/PersonOptsController.kt

@@ -0,0 +1,43 @@
+package inn.ocsf.bee.freigeld.serve.rest
+
+import inn.ocsf.bee.freigeld.core.model.PersonalizedId
+import inn.ocsf.bee.freigeld.serve.PersonService
+import inn.ocsf.bee.freigeld.utils.KeyValueBucket.personToLabel
+import inn.ocsf.bee.freigeld.utils.KeyValueStorage
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.*
+import java.util.*
+import javax.inject.Inject
+
+@RestController
+class PersonOptsController {
+
+    @Inject
+    private lateinit var kvStore: KeyValueStorage
+
+    @Inject
+    private lateinit var personService: PersonService
+
+    @RequestMapping("api/old/person/{personId}/opts/label/{labelId}", method = [RequestMethod.GET])
+    fun getLabel(@PathVariable("personId") personId: UUID, @PathVariable("labelId") labelId: String): ResponseEntity<LabelResp?> {
+        if (personId != personService.getCurrentPerson()?.id) throw IllegalAccessException("wrong person")
+        val lv = kvStore.get<String>(personToLabel, PersonalizedId(personId, labelId))?.let { LabelResp(labelId, it) }
+        return ResponseEntity.ok(lv)
+    }
+
+    @RequestMapping("api/old/person/{personId}/opts/label/{labelId}", method = [RequestMethod.POST])
+    fun putLabel(@PathVariable("personId") personId: UUID, @PathVariable("labelId") labelId: String, @RequestBody req: LabelReq): ResponseEntity<LabelResp?> {
+        if (personId != personService.getCurrentPerson()?.id) throw IllegalAccessException("wrong person")
+        if (req.label != null) {
+            kvStore.put(personToLabel, PersonalizedId(personId, labelId), req.label)
+        } else {
+            kvStore.remove(personToLabel, PersonalizedId(personId, labelId))
+        }
+        return getLabel(personId, labelId)
+    }
+
+}
+
+data class LabelReq(val label: String?)
+
+data class LabelResp(val labelId: String, val label: String)

+ 49 - 5
src/main/kotlin/inn/ocsf/bee/freigeld/utils/KeyValueStorage.kt

@@ -6,6 +6,7 @@ import org.springframework.data.mongodb.core.mapping.Document
 import org.springframework.data.mongodb.repository.MongoRepository
 import org.springframework.stereotype.Service
 import java.util.*
+import javax.annotation.PostConstruct
 import javax.inject.Inject
 
 @Service
@@ -14,12 +15,45 @@ class KeyValueStorage {
     @Inject
     private lateinit var keyValueRepo: KeyValueRepository
 
+    @PostConstruct
+    fun init() {
+        keyValueRepo.findAll().forEach {
+            if (it.keyStr == null) it.keyStr = getKeyStr(it.key!!)
+            keyValueRepo.save(it)
+        }
+    }
+
+    private fun getKeyStr(key: Any): String {
+        val keyStr = when (key) {
+            is KeyValueId -> key.getKey()
+            else -> key.toString()
+        }
+        return keyStr
+    }
+
     fun <T> get(bucket: KeyValueBucket, key: Any): T? {
-        return keyValueRepo.findOneByBucketAndKey(bucket, key).map { it.value as T }.orElse(null)
+        return keyValueRepo.findOneByBucketAndKeyStr(bucket, getKeyStr(key)).map { it.value as T }.orElse(null)
+    }
+
+    fun remove(bucket: KeyValueBucket, key: Any) {
+        keyValueRepo.deleteByBucketAndKeyStr(bucket, getKeyStr(key))
     }
 
     fun put(bucket: KeyValueBucket, key: Any, value: Any) {
-        keyValueRepo.save(KeyValueObject(bucket, key, value))
+        val keyStr = when (key) {
+            is KeyValueId -> key.getKey()
+            else -> key.toString()
+        }
+        val oldObj = keyValueRepo.findOneByBucketAndKeyStr(bucket, keyStr)
+        if (oldObj.isPresent) {
+            oldObj.ifPresent {
+                it.value = value
+                keyValueRepo.save(it)
+            }
+        } else {
+            keyValueRepo.save(KeyValueObject(bucket, keyStr, key, value))
+        }
+
     }
 
     fun <T> getByValue(bucket: KeyValueBucket, value: Any): List<T> {
@@ -34,13 +68,15 @@ class KeyValueStorage {
 enum class KeyValueBucket(val bucket: String) {
     personToSite("person-to-site"),
     emitterToPlan("emitter-to-plan"),
-    secretToSite("secret-to-site");
+    secretToSite("secret-to-site"),
+    personToLabel("person-to-label");
 }
 
 interface KeyValueRepository : MongoRepository<KeyValueObject, ObjectId> {
-    fun findOneByBucketAndKey(bucket: KeyValueBucket, key: Any): Optional<KeyValueObject>
+    fun findOneByBucketAndKeyStr(bucket: KeyValueBucket, key: String): Optional<KeyValueObject>
     fun findAllByBucketAndValue(bucket: KeyValueBucket, value: Any): Collection<KeyValueObject>
     fun findAllByBucket(bucket: KeyValueBucket): Collection<KeyValueObject>
+    fun deleteByBucketAndKeyStr(bucket: KeyValueBucket, key: String)
 }
 
 @Document("kv-store")
@@ -49,12 +85,20 @@ class KeyValueObject() {
     @Id
     var id: ObjectId? = null
     var bucket: KeyValueBucket? = null
+    var keyStr: String? = null
     var key: Any? = null
     var value: Any? = null
 
-    constructor(bucket: KeyValueBucket, key: Any, value: Any) : this() {
+    constructor(bucket: KeyValueBucket, keyStr: String, key: Any, value: Any) : this() {
         this.bucket = bucket
+        this.keyStr = keyStr
         this.key = key
         this.value = value
     }
+}
+
+abstract class KeyValueId {
+
+    abstract fun getKey(): String
+
 }