Procházet zdrojové kódy

change pw, change rank UI, auto-refresh on disconnect, new loading screen

master
peter před 5 roky
rodič
revize
96651913cd

+ 29
- 29
package-lock.json Zobrazit soubor

@@ -1050,9 +1050,9 @@
1050 1050
           }
1051 1051
         },
1052 1052
         "yallist": {
1053
-          "version": "3.0.3",
1054
-          "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
1055
-          "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
1053
+          "version": "3.1.1",
1054
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1055
+          "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1056 1056
           "dev": true
1057 1057
         }
1058 1058
       }
@@ -1476,9 +1476,9 @@
1476 1476
       "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg="
1477 1477
     },
1478 1478
     "cyclist": {
1479
-      "version": "0.2.2",
1480
-      "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz",
1481
-      "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=",
1479
+      "version": "1.0.1",
1480
+      "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
1481
+      "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
1482 1482
       "dev": true
1483 1483
     },
1484 1484
     "dashdash": {
@@ -5465,12 +5465,12 @@
5465 5465
       "dev": true
5466 5466
     },
5467 5467
     "parallel-transform": {
5468
-      "version": "1.1.0",
5469
-      "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz",
5470
-      "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=",
5468
+      "version": "1.2.0",
5469
+      "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz",
5470
+      "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==",
5471 5471
       "dev": true,
5472 5472
       "requires": {
5473
-        "cyclist": "~0.2.2",
5473
+        "cyclist": "^1.0.1",
5474 5474
         "inherits": "^2.0.3",
5475 5475
         "readable-stream": "^2.1.5"
5476 5476
       }
@@ -6130,9 +6130,9 @@
6130 6130
       }
6131 6131
     },
6132 6132
     "rpclibrary": {
6133
-      "version": "1.7.1",
6134
-      "resolved": "https://registry.npmjs.org/rpclibrary/-/rpclibrary-1.7.1.tgz",
6135
-      "integrity": "sha512-Ibo3qfURnQgZAq0eA2o+L9+PWaloJ4PHZs4ak42LL9fRgdWZXn33HAtowV0K2HVckbvrBvFqj/+PWTrWWBSOeg==",
6133
+      "version": "1.8.3",
6134
+      "resolved": "https://registry.npmjs.org/rpclibrary/-/rpclibrary-1.8.3.tgz",
6135
+      "integrity": "sha512-8pQRbMXQKCf3+v+XM01d+1Bw73pg5YzgyhW/ACpYON1I4re5fZAjWGPyLc+ms+MlYQIlKc3Uk92PGot2sbb85Q==",
6136 6136
       "requires": {
6137 6137
         "bsock": "^0.1.9",
6138 6138
         "http": "0.0.0",
@@ -6252,9 +6252,9 @@
6252 6252
       }
6253 6253
     },
6254 6254
     "serialize-javascript": {
6255
-      "version": "1.8.0",
6256
-      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.8.0.tgz",
6257
-      "integrity": "sha512-3tHgtF4OzDmeKYj6V9nSyceRS0UJ3C7VqyD2Yj28vC/z2j6jG5FmFGahOKMD9CrglxTm3tETr87jEypaYV8DUg==",
6255
+      "version": "2.1.2",
6256
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
6257
+      "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==",
6258 6258
       "dev": true
6259 6259
     },
6260 6260
     "serve-static": {
@@ -6503,9 +6503,9 @@
6503 6503
       }
6504 6504
     },
6505 6505
     "source-map-support": {
6506
-      "version": "0.5.13",
6507
-      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
6508
-      "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
6506
+      "version": "0.5.16",
6507
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
6508
+      "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
6509 6509
       "dev": true,
6510 6510
       "requires": {
6511 6511
         "buffer-from": "^1.0.0",
@@ -6637,9 +6637,9 @@
6637 6637
       }
6638 6638
     },
6639 6639
     "stream-shift": {
6640
-      "version": "1.0.0",
6641
-      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
6642
-      "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
6640
+      "version": "1.0.1",
6641
+      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
6642
+      "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
6643 6643
       "dev": true
6644 6644
     },
6645 6645
     "streamroller": {
@@ -6799,9 +6799,9 @@
6799 6799
       "dev": true
6800 6800
     },
6801 6801
     "terser": {
6802
-      "version": "4.2.0",
6803
-      "resolved": "https://registry.npmjs.org/terser/-/terser-4.2.0.tgz",
6804
-      "integrity": "sha512-6lPt7lZdZ/13icQJp8XasFOwZjFJkxFFIb/N1fhYEQNoNI3Ilo3KABZ9OocZvZoB39r6SiIk/0+v/bt8nZoSeA==",
6802
+      "version": "4.6.4",
6803
+      "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.4.tgz",
6804
+      "integrity": "sha512-5fqgBPLgVHZ/fVvqRhhUp9YUiGXhFJ9ZkrZWD9vQtFBR4QIGTnbsb+/kKqSqfgp3WnBwGWAFnedGTtmX1YTn0w==",
6805 6805
       "dev": true,
6806 6806
       "requires": {
6807 6807
         "commander": "^2.20.0",
@@ -6810,16 +6810,16 @@
6810 6810
       }
6811 6811
     },
6812 6812
     "terser-webpack-plugin": {
6813
-      "version": "1.4.1",
6814
-      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz",
6815
-      "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==",
6813
+      "version": "1.4.3",
6814
+      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
6815
+      "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
6816 6816
       "dev": true,
6817 6817
       "requires": {
6818 6818
         "cacache": "^12.0.2",
6819 6819
         "find-cache-dir": "^2.1.0",
6820 6820
         "is-wsl": "^1.1.0",
6821 6821
         "schema-utils": "^1.0.0",
6822
-        "serialize-javascript": "^1.7.0",
6822
+        "serialize-javascript": "^2.1.2",
6823 6823
         "source-map": "^0.6.1",
6824 6824
         "terser": "^4.1.2",
6825 6825
         "webpack-sources": "^1.4.0",

+ 1
- 1
package.json Zobrazit soubor

@@ -45,7 +45,7 @@
45 45
     "path": "^0.12.7",
46 46
     "reflect-metadata": "^0.1.13",
47 47
     "rimraf": "^3.0.0",
48
-    "rpclibrary": "^1.7.1",
48
+    "rpclibrary": "^1.8.3",
49 49
     "simple-git": "^1.124.0",
50 50
     "spawn-sync": "^2.0.0",
51 51
     "sqlite3": "^4.1.1",

+ 8
- 6
src/backend/Admin/Admin.ts Zobrazit soubor

@@ -74,11 +74,14 @@ implements TableDefinitionExporter, IAdmin {
74 74
     }
75 75
 
76 76
     stop(){
77
-        Promise.race([
78
-            Promise.all( this.frontworkComponents.map(c => c.stop?c.stop():undefined )),
79
-            new Promise((res, rej) => { setTimeout(res, 250);})
80
-        ]).catch(e => logger.warn(e))
81
-        .finally(() => { process.exit(0) })
77
+        Promise.all([ 
78
+            ...this.frontworkComponents.map(c => c.stop?c.stop():undefined ),
79
+        ])
80
+        .catch(e => logger.warn(e))
81
+        .finally(() => { 
82
+            this.rpcServer.destroy()
83
+            process.exit(0)
84
+         })
82 85
     }
83 86
 
84 87
     protected configChangeHandler = (conf:AdminConf, key?:string) => {
@@ -202,7 +205,6 @@ implements TableDefinitionExporter, IAdmin {
202 205
                     return await this.knex.schema.createTable(def.name, def.tableBuilder)
203 206
             })
204 207
         )
205
-        
206 208
         return this.knex
207 209
     }
208 210
 }

+ 1
- 1
src/backend/Components/Item/RPCInterface.ts Zobrazit soubor

@@ -18,6 +18,6 @@ export type ItemManagerFeatureIfc = {
18 18
     }
19 19
     managePriorities: {
20 20
         setPriority:IItemManager['setPriority']
21
-        deletePriorits: IItemManager['deletePriority']
21
+        deletePriority: IItemManager['deletePriority']
22 22
     }
23 23
 }

+ 3
- 1
src/backend/Components/User/Interface.ts Zobrazit soubor

@@ -11,11 +11,13 @@ export class IUserManager{
11 11
     checkToken: (token: string, rank: Rank) => boolean
12 12
     getUserRecordByToken: (tokenValue: string) => UserRecord | void
13 13
     getUser: (username: string) => Promise<User | void>
14
-
14
+    adminLogout: (username: string) => Promise<void>
15 15
     decrementCurrency: (user: User, tier:Tiers, value: number) => Promise<void>
16 16
     incrementCurrency: (user: User, tier:Tiers, value: number) => Promise<void>
17 17
     setCurrency: (user: User, tier:Tiers, value: number) => Promise<void>
18 18
     getCurrency: (user:User, tier:Tiers) => Promise<number>
19 19
     changeRank: (user:User, rank: Rank) => Promise<User>
20 20
     wipeCurrency: () => Promise<void>
21
+    changePassword: (userToken: string, pwHash: string) => Promise<User>
22
+    adminChangePassword: (user:User, pwHash: string) => Promise<User>
21 23
 }

+ 7
- 1
src/backend/Components/User/RPCInterface.ts Zobrazit soubor

@@ -8,14 +8,20 @@ export type UserManagerIfc = {
8 8
         createUser: IUserManager['createUser']
9 9
         getAuth: IUserManager['getAuth']
10 10
         getUser: IUserManager['getUser']
11
+        changePassword: IUserManager['changePassword']
11 12
     }
12 13
 }
13 14
 
14 15
 export type UserManagerFeatureIfc = {
16
+    manageUser: {
17
+        changeRank: IUserManager['changeRank']
18
+        adminLogout: IUserManager['adminLogout']
19
+        adminChangePassword: IUserManager['adminChangePassword']
20
+    }
21
+    
15 22
     modifyPermissions: {
16 23
         setPermission: IUserManager['setPermission']
17 24
         getPermissions: IUserManager['getPermissions']
18
-        changeRank: IUserManager['changeRank']
19 25
     }
20 26
 
21 27
     softreserveCurrency: {

+ 69
- 36
src/backend/Components/User/UserManager.ts Zobrazit soubor

@@ -13,6 +13,10 @@ import { IUserManager } from "./Interface";
13 13
 import { getLogger, Logger } from "log4js";
14 14
 import { saltedHash } from "../../Util/hash";
15 15
 import { _Tiers, Tiers } from "../../Types/Items";
16
+import { ICharacterManager } from "../Character/Interface";
17
+import { IRaidManager } from "../Raid/Interface";
18
+import { IItemManager } from "../Item/Interface";
19
+import { IGuildManager } from "../Guild/Interface";
16 20
 
17 21
 const uuid = require('uuid/v4')
18 22
 
@@ -34,16 +38,16 @@ implements FrontworkComponent<UserManagerIfc, UserManagerFeatureIfc>, IUserManag
34 38
     @Inject(IAdmin)
35 39
     private admin: FrontworkAdmin
36 40
 
37
-    @Inject(GuildManager)
41
+    @Inject(IGuildManager)
38 42
     private guild : GuildManager
39 43
     
40
-    @Inject(ItemManager)
44
+    @Inject(IItemManager)
41 45
     private item : ItemManager
42 46
     
43
-    @Inject(RaidManager)
47
+    @Inject(IRaidManager)
44 48
     private raid : RaidManager
45 49
     
46
-    @Inject(CharacterManager)
50
+    @Inject(ICharacterManager)
47 51
     private character : CharacterManager
48 52
     
49 53
     exporters :any[] = []
@@ -56,15 +60,23 @@ implements FrontworkComponent<UserManagerIfc, UserManagerFeatureIfc>, IUserManag
56 60
         this.getAuth,
57 61
         this.checkToken,
58 62
         this.createUser,
59
-        this.getUser
63
+        this.getUser,
64
+        this.changePassword
60 65
     ]
61 66
 
62
-    exportRPCFeatures = () => [{
67
+    exportRPCFeatures = () => [
68
+    {
69
+        name: 'manageUser' as 'manageUser',
70
+        exportRPCs: () => [
71
+            this.adminLogout,
72
+            this.changeRank,
73
+            this.adminChangePassword
74
+        ]
75
+    },{
63 76
         name: 'modifyPermissions' as 'modifyPermissions',
64 77
         exportRPCs: () => [
65 78
             this.getPermissions,
66 79
             this.setPermission,
67
-            this.changeRank
68 80
         ]
69 81
     },{
70 82
         name: 'softreserveCurrency' as 'softreserveCurrency',
@@ -84,6 +96,8 @@ implements FrontworkComponent<UserManagerIfc, UserManagerFeatureIfc>, IUserManag
84 96
             rank: rank
85 97
         })
86 98
 
99
+        await this.adminLogout(user.username)
100
+
87 101
         return await this.admin
88 102
         .knex('users')
89 103
         .where({
@@ -132,8 +146,6 @@ implements FrontworkComponent<UserManagerIfc, UserManagerFeatureIfc>, IUserManag
132 146
         })))
133 147
 
134 148
         //start rankServers
135
-        getLogger('UserManager').debug('Starting rank servers')
136
-
137 149
         let rankServers = { } as any
138 150
         await Promise.all(_Rank.map(async (r,i) => {
139 151
             const port = 20001 + i
@@ -145,25 +157,23 @@ implements FrontworkComponent<UserManagerIfc, UserManagerFeatureIfc>, IUserManag
145 157
             }
146 158
         }))
147 159
         this.rankServers = rankServers
148
-        
149
-    
160
+        getLogger('UserManager').debug(Object.values(this.rankServers).length+" rank servers started")
150 161
         setInterval(this.checkExpiredSessions, 600_000)   
151 162
     }
152 163
 
153 164
     stop = async () => {
154
-        Object.values(this.userLogins).forEach(x => Object.values(x.connections).forEach(c => c.destroy()))
155
-
156
-        await Promise.all(Object
157
-            .values(this.rankServers)
158
-            .map(async state => {
159
-                try{
160
-                    //return await state.server.destroy()
161
-                }catch(e){
162
-                    getLogger('UserManager').warn(e)
163
-                }
164
-            })
165
-        );
166
-        
165
+        Object.values(this.userLogins).forEach(x => {
166
+            Object.values(x.connections).forEach(c => c.destroy())
167
+        })
168
+
169
+        Object.values(this.rankServers)
170
+        .map(state => {
171
+            try{
172
+                return state.server.destroy()
173
+            }catch(e){
174
+                getLogger('UserManager').warn(e)
175
+            }
176
+        })
167 177
     }
168 178
 
169 179
     checkExpiredSessions = () => {
@@ -198,6 +208,26 @@ implements FrontworkComponent<UserManagerIfc, UserManagerFeatureIfc>, IUserManag
198 208
         return true
199 209
     }
200 210
 
211
+    changePassword = async (userToken:string, pwHash:string) : Promise<User> => {
212
+        const record = this.getUserRecordByToken(userToken)
213
+        if(!record) return {} as User
214
+        return await this.adminChangePassword(record.user, pwHash)
215
+    }
216
+
217
+    adminChangePassword = async(user:User, pwHash:string) : Promise<User> => {
218
+        const salted = await saltedHash(pwHash, salt)
219
+        await this.admin.knex('users')
220
+        .update({pwhash: salted})
221
+        .where({
222
+            username: user.username
223
+        })
224
+
225
+        const usr = await this.getUser(user.username)
226
+        if(!usr) return {} as any
227
+        await this.adminLogout(usr.username)
228
+        return usr
229
+    }
230
+
201 231
     setPermission = async (permission: RPCPermission) => {
202 232
         await this.admin.knex('rpcpermissions')
203 233
         .where('rpcname', '=', permission.rpcname)
@@ -270,21 +300,27 @@ implements FrontworkComponent<UserManagerIfc, UserManagerFeatureIfc>, IUserManag
270 300
     logout = async (username:string, tokenValue : string) : Promise<void> => {
271 301
         try{
272 302
             if(!this.checkTokenOwnedByUser(username, tokenValue)) return
303
+            return await this.adminLogout(username)
304
+        }catch(e){
273 305
 
306
+        }
307
+    }
308
+
309
+    adminLogout = async(username: string) : Promise<void> => {
310
+        try{
274 311
             if(this.userLogins[username]){
275 312
                 await Promise.all (Object.values(this.userLogins[username].connections).map(async (sock) => {
276
-                    await sock.call('navigate', '/auth/login')
313
+                    await sock.call('kick')
277 314
                 }))
278
-            }
279
-
280
-            Object.values(this.rankServers)
315
+            
316
+                Object.values(this.rankServers)
281 317
                 .forEach(state => {
282
-                    state.allowed = state.allowed.filter(allowed => allowed !== tokenValue)
283
-            })
284
-
285
-            delete this.userLogins[username]
318
+                    state.allowed = state.allowed.filter(allowed => allowed !== this.userLogins[username].auth.token.value)
319
+                })
320
+                delete this.userLogins[username]
321
+            }
286 322
         }catch(e){
287
-            console.log(e)
323
+            getLogger('UserManager').warn(e)
288 324
         }
289 325
     }
290 326
 
@@ -390,9 +426,6 @@ implements FrontworkComponent<UserManagerIfc, UserManagerFeatureIfc>, IUserManag
390 426
         username = username.toLowerCase()
391 427
         const maybeRecord = this.getUserRecordByToken(tokenValue)
392 428
         if(!maybeRecord || maybeRecord.auth.user.username != username){
393
-            getLogger('UserManager').warn(`Bad logout attempt
394
-            token by: ${maybeRecord?maybeRecord.auth.user.username:tokenValue}
395
-            tried to logout: ${username}`)
396 429
             return false
397 430
         } 
398 431
         return true

+ 7
- 6
src/backend/Injector/Injector.ts Zobrazit soubor

@@ -9,7 +9,7 @@ import { FrontworkComponent } from '../Types/FrontworkComponent';
9 9
 export const Injector = new class {
10 10
 
11 11
   injectionQueue :any[] = []
12
-
12
+  rootObj: any
13 13
   rootInterface : Type<any>
14 14
   root : Type<any>
15 15
   rootModules: Type<any>[] = []
@@ -29,6 +29,7 @@ export const Injector = new class {
29 29
       return this.moduleObjs[target.name] as any
30 30
 
31 31
     if(target.name === this.rootInterface.name || target.name === this.root.name){
32
+      if(this.rootObj) return this.rootObj
32 33
       let modules = this.modules.map(m => {
33 34
         const module = new m.implementation()
34 35
         if(m.implements)
@@ -36,10 +37,10 @@ export const Injector = new class {
36 37
         this.moduleObjs[m.implementation.name] = module
37 38
         return module
38 39
       })
39
-      const rootobj = new this.root(modules);
40
+      const rootObj = new this.root(modules);
40 41
 
41
-      this.moduleObjs[this.rootInterface.name] = rootobj
42
-      this.moduleObjs[target.name] = rootobj
42
+      this.moduleObjs[this.rootInterface.name] = rootObj
43
+      this.moduleObjs[target.name] = rootObj
43 44
       while(this.injectionQueue.length > 0){
44 45
         const i = this.injectionQueue.shift()
45 46
         if(this.moduleObjs[i.what.name])
@@ -47,8 +48,8 @@ export const Injector = new class {
47 48
         else
48 49
           this.injectionQueue.push(i)
49 50
       }
50
-
51
-      return rootobj
51
+      this.rootObj = rootObj
52
+      return rootObj
52 53
     }
53 54
     this.moduleObjs[target.name] = new target()
54 55
     return this.moduleObjs[target.name] as any   

+ 2
- 6
src/frontend/angular.json Zobrazit soubor

@@ -9,12 +9,8 @@
9 9
       "projectType": "application",
10 10
       "architect": {
11 11
         "build": {
12
-          "builder": "@angular-builders/custom-webpack:browser",
12
+          "builder": "@angular-devkit/build-angular:browser",
13 13
           "options": {
14
-            "customWebpackConfig": {
15
-              "path": "./custom-webpack.config.js",
16
-              "replaceDuplicatePlugins": true
17
-            },
18 14
             "preserveSymlinks": true,
19 15
             "rebaseRootRelativeCssUrls": true,
20 16
             "outputPath": "dist",
@@ -79,7 +75,7 @@
79 75
           }
80 76
         },
81 77
         "serve": {
82
-          "builder": "@angular-builders/custom-webpack:dev-server",
78
+          "builder": "@angular-devkit/build-angular:dev-server",
83 79
           "options": {
84 80
             "browserTarget": "ngx-admin-demo:build"
85 81
           },

+ 0
- 4
src/frontend/custom-webpack.config.js Zobrazit soubor

@@ -1,4 +0,0 @@
1
-module.exports = {
2
-    externals: ['http', 'fs'],
3
-    node: {global: true, fs: 'empty'}
4
-}

+ 14184
- 10876
src/frontend/package-lock.json
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 5
- 5
src/frontend/package.json Zobrazit soubor

@@ -50,7 +50,7 @@
50 50
     "angular2-chartjs": "0.4.1",
51 51
     "angular2-toaster": "^7.0.0",
52 52
     "bootstrap": "4.3.1",
53
-    "chart.js": "2.7.1",
53
+    "chart.js": "^2.9.3",
54 54
     "ckeditor": "4.7.3",
55 55
     "classlist.js": "1.1.20150312",
56 56
     "core-js": "2.5.1",
@@ -65,11 +65,11 @@
65 65
     "ng2-smart-table": "1.3.5",
66 66
     "ngx-cookie-service": "^2.2.0",
67 67
     "ngx-echarts": "^4.0.1",
68
-    "node-sass": "^4.12.0",
68
+    "node-sass": "^4.13.1",
69 69
     "normalize.css": "6.0.0",
70 70
     "pace-js": "1.0.2",
71 71
     "roboto-fontface": "0.8.0",
72
-    "rpclibrary": "^1.7.1",
72
+    "rpclibrary": "^1.8.3",
73 73
     "rxjs": "6.5.2",
74 74
     "rxjs-compat": "6.3.0",
75 75
     "socicon": "3.0.5",
@@ -80,11 +80,11 @@
80 80
     "zone.js": "~0.9.1"
81 81
   },
82 82
   "devDependencies": {
83
-    "@angular-devkit/build-angular": "~0.800.2",
83
+    "@angular-devkit/build-angular": "~0.803.24",
84 84
     "@angular/cli": "^8.0.2",
85 85
     "@angular/compiler-cli": "^8.0.0",
86 86
     "@angular/language-service": "8.0.0",
87
-    "@compodoc/compodoc": "1.0.1",
87
+    "@compodoc/compodoc": "^1.1.11",
88 88
     "@fortawesome/fontawesome-free": "^5.2.0",
89 89
     "@types/d3-color": "1.0.5",
90 90
     "@types/googlemaps": "^3.30.4",

+ 0
- 5
src/frontend/src/app/@theme/components/header/header.component.html Zobrazit soubor

@@ -1,9 +1,4 @@
1 1
 <div class="header-container">
2
-  <div>
3
-    <a (click)="toggleSidebar()" href="#" class="sidebar-toggle">
4
-      <nb-icon icon="menu-2-outline"></nb-icon>
5
-    </a>
6
-  </div>
7 2
   <div class="logo-container">
8 3
     <a (click)="toggleSidebar()" href="#" class="sidebar-toggle">
9 4
       <nb-icon icon="menu-2-outline"></nb-icon>

+ 1
- 6
src/frontend/src/app/frontcraft/auth/login/login.component.html Zobrazit soubor

@@ -35,12 +35,7 @@
35 35
            [maxlength]="15"
36 36
            [attr.aria-invalid]="password.invalid && password.touched ? true : null">
37 37
   </div>
38
-
39
-  <div class="form-control-group accept-group">
40
-    <nb-checkbox name="rememberMe" [(ngModel)]="user.rememberMe" *ngIf="rememberMe">Remember me</nb-checkbox>
41
-    <a class="forgot-password" routerLink="../request-password">Forgot Password?</a>
42
-  </div>
43
-
38
+  <br />
44 39
   <button nbButton
45 40
           fullWidth
46 41
           status="primary"

+ 12
- 8
src/frontend/src/app/frontcraft/auth/register/register.component.html Zobrazit soubor

@@ -35,28 +35,32 @@
35 35
            [attr.aria-invalid]="password.invalid && password.touched ? true : null">
36 36
   </div>
37 37
 
38
-  <div class="form-control-group accept-group">
39
-    <nb-checkbox name="rememberMe" [(ngModel)]="user.rememberMe" *ngIf="rememberMe">Remember me</nb-checkbox>
40
-  </div>
41
-
42 38
   <nb-card>
43 39
     <nb-card-body>
44 40
       <nb-checkbox [(ngModel)]="showApplication" name="amMember" #checkbox><!--I am already a member-->I understand this is super beta and things might not work or are not very pretty</nb-checkbox>
45 41
       <br />
46 42
       
47 43
       <span > <!--*ngIf="showApplication"-->
48
-        And my main is &nbsp; <input name="preMember" [(ngModel)]="character.name"  nbInput>, a 
44
+        And my main is &nbsp; <input nbInput [required]="true" name="preMember" [(ngModel)]="character.name"  >, a 
49 45
         <nb-select [(selected)]="character.race" placeholder="Race" (selectedChange)="onSelectRace()">
50 46
           <nb-option *ngFor="let race of races" [value]="race">{{race}}</nb-option>
51 47
         </nb-select>
52
-        <nb-select [(selected)]="character.spec" placeholder="Spec" (selectedChange)="onSelectSpec()">
48
+        <nb-select 
49
+          [(selected)]="character.spec" 
50
+          placeholder="Spec" 
51
+          (selectedChange)="onSelectSpec()">
53 52
           <nb-option *ngFor="let spec of specs" [value]="spec">{{spec}}</nb-option>
54 53
         </nb-select>
55
-        <nb-select [(selected)]="character.class" placeholder="Class" (selectedChange)="onSelectClass()">
54
+        <nb-select 
55
+          [(selected)]="character.class" 
56
+          placeholder="Class" 
57
+          (selectedChange)="onSelectClass()">
56 58
           <nb-option *ngFor="let class of classes"  [value]="class">{{class}}</nb-option>
57 59
         </nb-select>
58 60
          with rank 
59
-        <nb-select [(selected)]="user.rank" placeholder="Rank">
61
+        <nb-select 
62
+        [(selected)]="user.rank" 
63
+        placeholder="Rank">
60 64
           <nb-option *ngFor="let rank of ranks" [value]="rank" >{{rank}}</nb-option>
61 65
         </nb-select>
62 66
       </span>

+ 1
- 1
src/frontend/src/app/frontcraft/pages/raid/raid.component.ts Zobrazit soubor

@@ -195,7 +195,7 @@ export class FrontcraftRaidComponent implements OnInit, OnDestroy{
195 195
         this.displayedtokens = this.tokens
196 196
       }else{
197 197
         this.displayedtokens = {}
198
-        Object.entries(this.tokens).forEach((e: [string, (Character & SRToken & Item)[]]) => {
198
+        Object.entries(this.tokens).forEach((e: [string /*itemname*/, (Character & SRToken & Item)[]]) => {
199 199
           const filteredTokens = e[1].filter(item => {
200 200
             return item.itemname.toLocaleLowerCase().includes(this.search.toLocaleLowerCase())
201 201
           })

+ 5
- 3
src/frontend/src/app/frontcraft/pages/shop/buytoken.component.ts Zobrazit soubor

@@ -9,7 +9,7 @@ import { _Tiers, allItems, Tiers } from '../../../../../../backend/Types/Items';
9 9
 @Component({
10 10
     selector: 'buyToken',
11 11
     template: `
12
-    <nb-card class="col-12 col-xl-9">
12
+    <nb-card style="width: 500px">
13 13
       <nb-card-header>
14 14
         Buy {{item.itemname}}
15 15
       </nb-card-header>
@@ -19,8 +19,10 @@ import { _Tiers, allItems, Tiers } from '../../../../../../backend/Types/Items';
19 19
         </p>  
20 20
         <div *ngIf="currency>0">
21 21
             <nb-alert accent="danger" *ngIf="invalidatedTokens.length > 0">
22
-                Claiming this reserve invalidates the following streaks. <br />
23
-                This is not reversible.
22
+                Claiming this reserve puts the following streaks to zero <br />
23
+                <span>
24
+                  <nb-icon icon="alert-triangle-outline" status="warning"></nb-icon>&nbsp;This is not reversible&nbsp;<nb-icon icon="alert-triangle-outline" status="warning"></nb-icon>
25
+                </span>
24 26
                 <ul *ngFor="let item of invalidatedTokens">
25 27
                     <li>
26 28
                         [ {{item.level}} ] 

+ 58
- 5
src/frontend/src/app/frontcraft/pages/user/user.component.html Zobrazit soubor

@@ -6,12 +6,65 @@ accent="info">
6 6
         <h2 style="text-transform: capitalize;">{{user.username}}</h2>
7 7
     </nb-card-header>
8 8
     <nb-card-body>
9
-        {{user.MC}} MC
10
-        {{user.BWL}} BWL
11
-        {{user.ZG}} ZG
12
-        {{user.AQ20}} AQ20
13
-        {{user.AQ40}} AQ40
9
+
10
+        <h3>Rank</h3>
11
+        <span *ngIf="!manageUser">
12
+            {{user.rank}}
13
+        </span>
14
+        <nb-select 
15
+            *ngIf="manageUser"
16
+            [(selected)]="user.rank" 
17
+            placeholder="Rank"
18
+            (selectedChange)="onSelectRank()">
19
+
20
+          <nb-option *ngFor="let rank of ranks" [value]="rank" >{{rank}}</nb-option>
21
+        </nb-select>
22
+
23
+        <h3 *ngIf="myProfile || manageUser">Password</h3>
24
+        <form (ngSubmit)="submitPasswordChange()" #form="ngForm" aria-labelledby="title" *ngIf="myProfile || manageUser">
25
+            <input nbInput
26
+                fullWidth
27
+                type="password"
28
+                [(ngModel)]="newPw"
29
+                name="newPw"
30
+                id="input-newPw"
31
+                pattern=".+"
32
+                placeholder="New Password"
33
+                fieldSize="small"
34
+                autofocus
35
+                [required]="true">
36
+
37
+            <input nbInput
38
+                fullWidth
39
+                type="password"
40
+                [(ngModel)]="newPwConfirm"
41
+                name="newPwConfirm"
42
+                id="input-newPwConfirm"
43
+                pattern=".+"
44
+                placeholder="Confirm Password"
45
+                fieldSize="small"
46
+                autofocus
47
+                [required]="true">
48
+
49
+            <button nbButton
50
+                fullWidth
51
+                status="primary"
52
+                size="small"
53
+                [disabled]="!form.valid || newPw != newPwConfirm"
54
+                [class.btn-pulse]="submitted">
55
+                Change
56
+            </button>
57
+        </form>
58
+        <br />
59
+
60
+        <h3>Avaiable Reserves</h3>
61
+        {{user.MC}} MC |
62
+        {{user.BWL}} BWL |
63
+        {{user.ZG}} ZG |
64
+        {{user.AQ20}} AQ20 |
65
+        {{user.AQ40}} AQ40 |
14 66
         {{user.Naxx}} Naxx
67
+        <br />
15 68
       <ng-container *ngFor="let char of characters">
16 69
           <character [name]="char.charactername" [link]="'character'"></character>
17 70
       </ng-container>

+ 34
- 3
src/frontend/src/app/frontcraft/pages/user/user.component.ts Zobrazit soubor

@@ -1,9 +1,11 @@
1 1
 import { Component, OnInit } from '@angular/core';
2
-import { ApiService } from '../../services/login-api';
2
+import { ApiService, hash } from '../../services/login-api';
3 3
 import { Router, ActivatedRoute } from '@angular/router';
4 4
 import { User, Spec, Character } from '../../../../../../backend/Types/Types';
5 5
 import { getClassColor } from '../../../../../../backend/Types/PlayerSpecs';
6 6
 import { _Tiers } from '../../../../../../backend/Types/Items';
7
+import { _Rank } from '../../../../../../backend/Types/Types';
8
+import { NbToastrService } from '@nebular/theme';
7 9
 
8 10
 @Component({
9 11
   selector: 'user-component',
@@ -11,22 +13,31 @@ import { _Tiers } from '../../../../../../backend/Types/Items';
11 13
 })
12 14
 export class FrontcraftUserComponent implements OnInit{
13 15
 
16
+  newPw
17
+  newPwConfirm
18
+
19
+  ranks = _Rank
20
+  myProfile = false
21
+  manageUser
14 22
   user:User = {} as any
15 23
   characters : (Character & Spec)[] = []
16 24
 
17 25
   constructor(
18 26
     private route: ActivatedRoute,
19
-    private api : ApiService
27
+    private api : ApiService,
28
+    private toastr: NbToastrService
20 29
   ){}
21 30
 
22 31
   async ngOnInit(){
23 32
     const param = this.route.snapshot.paramMap.get('name');
24 33
     const usr = await this.api.get('UserManager').getUser(param)
25 34
 
26
-
27 35
     if(!usr) return
28 36
     this.user = usr
29 37
 
38
+    this.manageUser = this.api.get('manageUser')
39
+    this.myProfile = this.api.getCurrentUser().username === this.user.username
40
+    
30 41
     const characters = await this.api.get('CharacterManager').getCharactersOfUser(this.user.username)
31 42
     characters.forEach(async c => {
32 43
       const tokens = await this.api.get('ItemManager').getTokens(c, _Tiers, true)
@@ -35,4 +46,24 @@ export class FrontcraftUserComponent implements OnInit{
35 46
     })
36 47
     this.characters = characters
37 48
   }
49
+
50
+  async onSelectRank(){
51
+    const manage = this.api.get('manageUser')
52
+    if(!manage) return
53
+    await manage.changeRank(this.user, this.user.rank)
54
+    this.toastr.success("Rank changed to "+this.user.rank, "Update success")
55
+  }
56
+
57
+  async submitPasswordChange(){
58
+    const pw = await hash(this.newPw)
59
+    this.newPw = null
60
+    this.newPwConfirm = null
61
+
62
+    const manage = this.api.get('manageUser')
63
+    if(!manage){
64
+      await this.api.get('UserManager').changePassword(this.api.getAuth().token.value, pw)
65
+    }else{
66
+      await manage.adminChangePassword(this.user, pw)
67
+    }
68
+  }
38 69
 }

+ 58
- 15
src/frontend/src/app/frontcraft/services/login-api.ts Zobrazit soubor

@@ -6,6 +6,7 @@ import { CookieService } from 'ngx-cookie-service';
6 6
 import { Router } from '@angular/router';
7 7
 import { ShoutMessage } from '../../../../../backend/Components/Shoutbox/Interface';
8 8
 import { saltedHash } from '../../../../../backend/Util/hash';
9
+import { NbToastrService } from '@nebular/theme';
9 10
 
10 11
 @Injectable()
11 12
 export class ApiService{
@@ -16,7 +17,8 @@ export class ApiService{
16 17
     constructor(
17 18
         private injector: Injector,
18 19
         private cookieSvc : CookieService,
19
-        private ngZone : NgZone
20
+        private ngZone : NgZone,
21
+        private toastr: NbToastrService
20 22
     ){}
21 23
 
22 24
     getUnprivilegedSocket = () : RPCSocket & FrontcraftIfc => this.socket
@@ -30,30 +32,34 @@ export class ApiService{
30 32
                 auth.port, 
31 33
                 window.location.hostname
32 34
             )
33
-           
34
-            const authSock = await sock.connect<RPCSocket & SomeOf<FrontcraftFeatureIfc>>(auth.token.value)
35 35
 
36 36
             sock.hook('kick', () => {
37
-                console.log("I got kicked");
37
+                this.logout()
38 38
             })
39
+
39 40
             sock.hook('getUserData', () => auth)
40 41
             sock.hook('navigate', (where:string) => {
41 42
                 this.ngZone.run( () => {
42 43
                     this.injector.get(Router).navigateByUrl(where)
43 44
                 })
44 45
             })
45
-            sock.on('error', (e) => {
46
-                sock.destroy();
47
-                
46
+
47
+            sock.on('close', async () => {
48
+                //handled via unprivileged socket
48 49
             })
50
+
51
+            sock.on('error', async (e) => {
52
+                //handled via unprivileged socket
53
+            })
54
+            const authSock = await sock.connect<RPCSocket & SomeOf<FrontcraftFeatureIfc>>(auth.token.value)
49 55
             
50 56
             this.auth = auth
51 57
             this.privSocket = authSock
52 58
             this.cookieSvc.set('token', JSON.stringify(auth))
53 59
             return authSock
54 60
         }catch(e){ 
55
-            //login failed
56
-            throw new Error(e)
61
+            if(this.privSocket) this.privSocket.destroy()
62
+            return;
57 63
         }
58 64
     }
59 65
 
@@ -76,12 +82,25 @@ export class ApiService{
76 82
 
77 83
     login = async (username: string, password: string) : Promise<RPCSocket & SomeOf<FrontcraftFeatureIfc>> => {
78 84
         const pwHash = await hash(password)
79
-        const auth = await this.socket.UserManager.login(username, pwHash)
85
+        let auth
86
+        try{
87
+            auth = await this.socket.UserManager.login(username, pwHash)
88
+        }catch(e){
89
+            this.toastr.danger("Login failed", "Error")
90
+            return
91
+        }
80 92
 
81 93
         if(!auth){ 
82 94
             await this.logout()
95
+            this.toastr.danger("Login failed", "Error")
83 96
             throw new Error("Login failed")
84 97
         }
98
+
99
+        //attach error handler now so failed logins dont trigger reloads
100
+        this.socket.on('error', async (err) => {
101
+            location.reload()
102
+        })
103
+
85 104
         const sock = await this.getPrivilegedSocket(auth)
86 105
         return sock
87 106
     }
@@ -93,26 +112,50 @@ export class ApiService{
93 112
 
94 113
     logout = async () => {
95 114
         this.cookieSvc.set('token', undefined)
96
-        if(this.auth) await this.socket.UserManager.logout(this.auth.user.username, this.auth.token.value)
115
+        if(this.auth){
116
+            try{
117
+                await this.socket.UserManager.logout(this.auth.user.username, this.auth.token.value)
118
+            }catch(e){
119
+                //socket is dead
120
+            }
121
+        } 
122
+
97 123
         if(this.privSocket) this.privSocket.destroy()
98 124
         this.privSocket = null
99 125
         this.auth  = null
126
+        
100 127
         this.ngZone.run(() => {
101 128
             this.injector.get(Router).navigate(['/auth']);
102 129
         })
103 130
     }
104 131
 
105 132
     initialize = async () : Promise<any> => {
106
-        const sock = await RPCSocket.makeSocket<FrontcraftIfc>(20000, window.location.hostname)
133
+        if(this.socket){
134
+            this.socket.destroy()
135
+            this.socket = null
136
+        }
137
+
138
+        try{
139
+            let conn = new RPCSocket(20000, window.location.hostname)
140
+            conn.on('close', async () => {
141
+            })
142
+
143
+            
144
+
145
+            this.socket = await conn.connect<FrontcraftIfc>()
146
+        }catch(e){
147
+            alert('Unable to connect. The server appears to be down.\nPress OK to refresh the page')
148
+            location.reload()
149
+            return;
150
+        }
107 151
 
108
-        this.socket = sock
109 152
         try{
110 153
             const cookie = JSON.parse(this.cookieSvc.get('token'))
111 154
             
112 155
             if(cookie != null) {
113 156
                 try{
114
-                    const auth = await sock.UserManager.getAuth(cookie.token.value)
115
-                    if(!auth) return sock
157
+                    const auth = await this.socket.UserManager.getAuth(cookie.token.value)
158
+                    if(!auth) return this.socket
116 159
                     return await this.getPrivilegedSocket(auth)
117 160
                 }catch(e){
118 161
                     await this.logout()

+ 35
- 12
src/frontend/src/index.html Zobrazit soubor

@@ -2,7 +2,35 @@
2 2
 <html>
3 3
 <head>
4 4
   <meta charset="utf-8">
5
-  <title>ngx-admin Demo Application</title>
5
+  <title>Frontcraft</title>
6
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat">
7
+  <style>
8
+  @import url('https://fonts.googleapis.com/css?family=Montserrat');
9
+
10
+  .loadTitle {
11
+    font-family: 'Montserrat';
12
+    text-align: center;
13
+    color: #FFF;
14
+    display: flex;
15
+    flex-direction: column;
16
+    align-items: center;
17
+    justify-content: center;
18
+    letter-spacing: 1px;
19
+    padding-top: calc(50vh - 120px);
20
+  }
21
+
22
+  .loadH {
23
+    background-image: url('https://media.giphy.com/media/26BROrSHlmyzzHf3i/giphy.gif');
24
+    background-size: cover;
25
+    color: transparent;
26
+    background-clip: text;
27
+    -moz-background-clip: text;
28
+    -webkit-background-clip: text;
29
+    text-transform: uppercase;
30
+    font-size: min(10vw, 120px);
31
+    margin: 10px 0;
32
+  }
33
+  </style>
6 34
 
7 35
   <base href="/">
8 36
 
@@ -11,17 +39,12 @@
11 39
   <link rel="icon" type="image/x-icon" href="favicon.ico">
12 40
 </head>
13 41
 <body>
14
-  <ngx-app>Loading...</ngx-app>
15
-
16
-  <style>@-webkit-keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@-moz-keyframes spin{0%{-moz-transform:rotate(0)}100%{-moz-transform:rotate(360deg)}}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.spinner{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1003;background: #000000;overflow:hidden}  .spinner div:first-child{display:block;position:relative;left:50%;top:50%;width:150px;height:150px;margin:-75px 0 0 -75px;border-radius:50%;box-shadow:0 3px 3px 0 rgba(255,56,106,1);transform:translate3d(0,0,0);animation:spin 2s linear infinite}  .spinner div:first-child:after,.spinner div:first-child:before{content:'';position:absolute;border-radius:50%}  .spinner div:first-child:before{top:5px;left:5px;right:5px;bottom:5px;box-shadow:0 3px 3px 0 rgb(255, 228, 32);-webkit-animation:spin 3s linear infinite;animation:spin 3s linear infinite}  .spinner div:first-child:after{top:15px;left:15px;right:15px;bottom:15px;box-shadow:0 3px 3px 0 rgba(61, 175, 255,1);animation:spin 1.5s linear infinite}</style>
17
-  <div id="nb-global-spinner" class="spinner">
18
-    <div class="blob blob-0"></div>
19
-    <div class="blob blob-1"></div>
20
-    <div class="blob blob-2"></div>
21
-    <div class="blob blob-3"></div>
22
-    <div class="blob blob-4"></div>
23
-    <div class="blob blob-5"></div>
42
+  <ngx-app></ngx-app>
43
+  <div id="nb-global-spinner" class="spinner" style="background-color: #111; height: 100vh;">
44
+    <div class="loadTitle">
45
+      <h1 class="loadH">Tranquil</h1>
46
+    </div>
24 47
   </div>
25
-
48
+  
26 49
 </body>
27 50
 </html>

+ 3
- 0
src/frontend/src/polyfills.ts Zobrazit soubor

@@ -55,3 +55,6 @@ import 'core-js/es7/object';
55 55
 if (typeof SVGElement.prototype.contains === 'undefined') {
56 56
   SVGElement.prototype.contains = HTMLDivElement.prototype.contains;
57 57
 }
58
+
59
+(window as any).global = window;
60
+(window as any).global.Buffer = (window as any).global.Buffer || require('buffer').Buffer;

+ 2
- 17
test/backendTest.ts Zobrazit soubor

@@ -53,6 +53,8 @@ const defaultPermissions = [
53 53
         rpcname: 'softreserveCurrency', ...adminsOnly
54 54
     }, {
55 55
         rpcname: 'manageRaid', ...adminsOnly
56
+    }, {
57
+        rpcname: 'manageUser', ...adminsOnly
56 58
     }]
57 59
 
58 60
 const testAccounts: protoAccount[] = [
@@ -271,12 +273,6 @@ describe('Frontcraft', () => {
271 273
                 undefined, 2, "str-to-ap bias"
272 274
             ),
273 275
 
274
-            makePrio(
275
-                'Band of Accuria',
276
-                { class: 'Warrior', specname: 'Protection' },
277
-                undefined, 2, "hit bias"
278
-            ),
279
-
280 276
             makePrio(
281 277
                 'Bracers of Arcane Accuracy',
282 278
                 { class: 'Warlock', specname: 'Demonology' },
@@ -376,22 +372,11 @@ describe('Frontcraft', () => {
376 372
                 undefined, 1, "hit bias"
377 373
             ),
378 374
 
379
-            makePrio(
380
-                'Chromatic Boots',
381
-                { class: 'Warrior', specname: 'Protection' },
382
-                undefined, 2, "hit bias"
383
-            ),
384
-
385 375
             makePrio(
386 376
                 'Crul\'shorukh, Edge of Chaos',
387 377
                 undefined,
388 378
                 "Human", -2, "Non-human"
389 379
             ),
390
-            makePrio(
391
-                'Crul\'shorukh, Edge of Chaos',
392
-                { class: 'Warrior', specname: 'Protection' },
393
-                undefined, 2, "nice dps bias"
394
-            ),
395 380
             makePrio(
396 381
                 'Crul\'shorukh, Edge of Chaos',
397 382
                 { class: 'Warrior', specname: 'Fury' },

Načítá se…
Zrušit
Uložit