Compare commits
	
		
			887 Commits
		
	
	
		
			0.0.5
			...
			b8767b8bd4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b8767b8bd4 | |||
| bf686e89e0 | |||
| 6163f0a90b | |||
| 8f0f795a70 | |||
| 22cae39bd3 | |||
| 0b07a53ed2 | |||
| d4a02e7db2 | |||
| b9a7dc84f0 | |||
| 7111068361 | |||
| 63964fbf2c | |||
| cbcb829fbd | |||
| 057ae0d797 | |||
| 257f320ee3 | |||
| 7b15c2d88b | |||
| 988f17a795 | |||
| 4471e57438 | |||
| 51daf969cf | |||
| cb71fcd13b | |||
| a6a526dc5d | |||
| dd6d799c84 | |||
| e89e07d0fc | |||
| c28843c405 | |||
| 4834a6698b | |||
| 69afd4d587 | |||
| 24d152fdc8 | |||
| 4279e43743 | |||
| d837654617 | |||
| 0767943721 | |||
| ca87774767 | |||
| f693f2cde9 | |||
| d70c5b1bbc | |||
| 71e3d0efe2 | |||
| b517dff8a8 | |||
| 114c246ace | |||
| d7703c9e07 | |||
| dc3071f7d2 | |||
| 5fb355f450 | |||
| 33c13de32c | |||
| 1be073a4fa | |||
| b0d8249452 | |||
| 7af883f271 | |||
| f5433076b0 | |||
| 6aafe4a6ae | |||
| bdeeb03645 | |||
| 675c8762e8 | |||
| 89e392473c | |||
| 6c9b91d75a | |||
| 8c00aefd6c | |||
| 3afd785a54 | |||
| 8099999e2c | |||
| a139554e05 | |||
| 0290b0e5f5 | |||
| 0f7fa990d4 | |||
| 2f568c9cb8 | |||
| 1cb2dc9d53 | |||
| 6005b0661f | |||
| 5a36c8dcae | |||
| 58f4d2151f | |||
| 95135ddc89 | |||
| a7fe1e1759 | |||
| 56a5f41686 | |||
| c23b4d907f | |||
| bd7b81efe7 | |||
| 274a146b9b | |||
| 5a3fc5b2bd | |||
| 070560e863 | |||
| 536900091a | |||
| 8154e715bb | |||
| 4c6665062f | |||
| cb3ea9b1eb | |||
| 7a64f23937 | |||
| 96ba25ec6c | |||
| e6a8ebcb5b | |||
| 888cab5898 | |||
| 383a8095b8 | |||
| 63f6526e4f | |||
| b24e24ff7d | |||
| 9ce35d8eb7 | |||
| 48a87e8936 | |||
| b8c28ebb08 | |||
| 5daaa3a73c | |||
| 24c38cce26 | |||
| bd00f4f8d5 | |||
| 03d76e6d0b | |||
| 3f8e8ce3a6 | |||
| c9bd6de476 | |||
| e702118d4d | |||
| 97159dd9f8 | |||
| 942d9dbc76 | |||
| 88844e1a44 | |||
| e76a9cef95 | |||
| 20aeed8778 | |||
| ccb7ae29a3 | |||
| dcb12b0ac2 | |||
| dd1258333e | |||
| 3ef3a94b20 | |||
| 135852eb9a | |||
| 963253cbc8 | |||
| 539a6509b1 | |||
| f3d73d5346 | |||
| f159252651 | |||
| 6ab60998d4 | |||
| 30d220bc36 | |||
| 24aff3bac4 | |||
| ce63043887 | |||
| e40017a6b8 | |||
| e843a464e7 | |||
| d0ae50d557 | |||
| 7a49e7c5c9 | |||
| 1dd64204cc | |||
| 438ff0fc3f | |||
| c1bbda51f0 | |||
| 4705a39aab | |||
| 4d721f62d9 | |||
| b0328ffdaf | |||
| 031cede542 | |||
| 3c69f8c4a8 | |||
| cc6568c381 | |||
| a3a1395a46 | |||
| b08acc6660 | |||
| 7a303c2b2c | |||
| 3f9a7049e3 | |||
| 6249419fae | |||
| f347b7ad49 | |||
| 74faec85c8 | |||
| fbdadbef1f | |||
| c87c97c90f | |||
| a6bca59ffe | |||
| 732a1b88d9 | |||
| 4c960feeb2 | |||
| 72fee96a08 | |||
| fcb43f92b0 | |||
| 5ba8f1dd44 | |||
| 3d3790c2eb | |||
| 1fa3fa75ee | |||
| c8882ae6a1 | |||
| 673e896aa3 | |||
| 0ed7f78b2c | |||
| 1d38d308ad | |||
| d709ee7479 | |||
| aae042c041 | |||
| ca7a84eb3e | |||
| 1f32ed0727 | |||
| 289f9e2196 | |||
| 937a9fad4d | |||
| 7c3a1b8fff | |||
| a8ea4fa659 | |||
| c1dd4518d1 | |||
| bdc7bb67e7 | |||
| 54988ba0fe | |||
| ce3ca9f1c8 | |||
| 46b7aceb0b | |||
| 486e450a58 | |||
| 623b5a1873 | |||
| a7958eecd6 | |||
| 13e839902c | |||
| 94001a48f1 | |||
| 2cb7ec7317 | |||
| 757332ed2b | |||
| 8ba7ee1d48 | |||
| c5178e0181 | |||
| a1a94ec9da | |||
| f7af777104 | |||
| 076aa87dba | |||
| ca6fa633a1 | |||
| 641e2aed52 | |||
| cba4455d53 | |||
| d5930f7c46 | |||
| 5541ae6ebd | |||
| 6c43872198 | |||
| e4ed20da3e | |||
| cb6e78fc17 | |||
| bf1ec976e3 | |||
| d0a7e34de8 | |||
| 08957d4dc2 | |||
| 1d762f5662 | |||
| a95a9b4ec4 | |||
| e5dab3469c | |||
| c01233b4d6 | |||
| 92920273be | |||
| 6bb3ae8ba9 | |||
| cedc1750c2 | |||
| 3f372123fd | |||
| a3437475ca | |||
| 83765136cc | |||
| e26b7d4923 | |||
| e7f0cb45c9 | |||
| ffcd45e572 | |||
| d7099717c2 | |||
| 66d6023335 | |||
| 5f5c8a061e | |||
| bf71e35ecd | |||
| 64da0eadb3 | |||
| 52728290b4 | |||
| 3f2a2d2929 | |||
| f1d85cfb85 | |||
| 15356c1030 | |||
| 82c65b632c | |||
| ae7d617690 | |||
| bf6b70106e | |||
| 33310cdb44 | |||
| db58a280b3 | |||
| 149f3a83b2 | |||
| a5d2a6ecd3 | |||
| bb9bad6d90 | |||
| ada679823c | |||
| 9a1678acf0 | |||
| 485c247cd3 | |||
| ddea02db57 | |||
| 1551a444ba | |||
| f289afd8bc | |||
| a9e06c9055 | |||
| c2fdfeed4f | |||
| 0342757d92 | |||
| 5833f4218f | |||
| 0fcc729b56 | |||
| a2c97a11a3 | |||
| aa833736d3 | |||
| 771a205fe6 | |||
| 6074ac5b3a | |||
| 030b2255d4 | |||
| f7f6df41ff | |||
| be397c8899 | |||
| dd3c9275d6 | |||
| 764b7ffe00 | |||
| d870b2fd01 | |||
| aaec09d2ab | |||
| bce8811925 | |||
| 3afc207903 | |||
| fca997beb8 | |||
| 39ebfbf0b6 | |||
| 3736b29e54 | |||
| b4c9369a53 | |||
| 5d6c8c957a | |||
| 09fe47b9aa | |||
| b4acd157fc | |||
| b1fced7764 | |||
| c0cafb4d51 | |||
| 45d61b487e | |||
| 28ef139a70 | |||
| 656f63dfd5 | |||
| ba3b5eeefc | |||
| ba396e0eba | |||
| 3c11d88557 | |||
| 305fa0078d | |||
| a46d14278b | |||
| 680ae8ebbb | |||
| cc869f69ad | |||
| b9aac71676 | |||
| a30a342e00 | |||
| bdcfce88cb | |||
| dd81f4c7e4 | |||
| 416f2a1366 | |||
| 5e353db206 | |||
| 0c9867d706 | |||
| 8379c3e29c | |||
| c4edccace7 | |||
| 74de6559d7 | |||
| a6f73c733c | |||
| ca3d093e54 | |||
| 28cfbaa662 | |||
| 90e1ad7db7 | |||
| 906a1dc9e7 | |||
| 5872c6335b | |||
| 701706c028 | |||
| 09bbc70f5f | |||
| dd9cb6d3ef | |||
| 23c732b690 | |||
| 656d564baa | |||
| f3f5cb462e | |||
| 9959172f2a | |||
| 8f0a396dd0 | |||
| a18d4d3cee | |||
| 390b36dfd4 | |||
| 3b718f3ce5 | |||
| 321b20b073 | |||
| f7a0ec7174 | |||
| 110a84783e | |||
| 333e806da4 | |||
| f4f621973a | |||
| bcad691045 | |||
| 74791df68b | |||
| 8425043099 | |||
| 74b982afba | |||
| 3aefa75412 | |||
| 71cab4e836 | |||
| 4e10077901 | |||
| c32fa93673 | |||
| 3d1baae0cc | |||
| 94dd7963b7 | |||
| 7ba67b9dca | |||
| 6e5f1bd5ff | |||
| 60ee6ebc1e | |||
| 02295346da | |||
| c4ea808e06 | |||
| ff7406e71a | |||
| 8dc2810c0c | |||
| ff8af090e3 | |||
| bcc15e4286 | |||
| 2a87819486 | |||
| 9d5e486c6d | |||
| e44cc4c4cb | |||
| 581ca5ff6c | |||
| b972395ae8 | |||
| e5f4f6ee59 | |||
| fea4857685 | |||
| f9e75d06b8 | |||
| 38223b194b | |||
| 09b24aa609 | |||
| 348e6cdec7 | |||
| bd1813a0e8 | |||
| e07f258a31 | |||
| 61bbeb0d8f | |||
| 650a55e586 | |||
| 2071c4db33 | |||
| 80e606aa96 | |||
| 20f960ed67 | |||
| e6fe8fcd58 | |||
| 870fd47c83 | |||
| 644045db44 | |||
| 8611fcb849 | |||
| 08e6e59655 | |||
| ae74b3963f | |||
| 54ed313342 | |||
| ad4b903c25 | |||
| 9bd7636a23 | |||
| b94179e3ca | |||
| 827002989e | |||
| eeff67c192 | |||
| 583a4bc0dd | |||
| 53fcff77d0 | |||
| 1f0c842d9e | |||
| 13ccab5e28 | |||
| b5018eb114 | |||
| aedfcfcc83 | |||
| db0876015b | |||
| 69417e93c0 | |||
| f71a22f4dd | |||
| 570c34bed0 | |||
| 7be2971a9e | |||
| b92f633d68 | |||
| d3647e3399 | |||
| 389e423850 | |||
| 46af786516 | |||
| b4c117b7dc | |||
| 5cade25eeb | |||
| fb77f4d798 | |||
| c116338cd7 | |||
| 979d36ea91 | |||
| c43334bf96 | |||
| 71c4caae8b | |||
| 536de2a319 | |||
| e26744b792 | |||
| d02e9dec56 | |||
| 637975305f | |||
| c418603423 | |||
| 78d2ac3027 | |||
| 470703c4de | |||
| e260e16d66 | |||
| 6b0155f014 | |||
| 33890b544b | |||
| d7ea928714 | |||
| 908ac4f1ce | |||
| cf012c0b7e | |||
| 71898d576c | |||
| c964591839 | |||
| cc4bf4451c | |||
| 7dbbd3780d | |||
| 3697783e19 | |||
| 161feaf364 | |||
| 75e2a44c9c | |||
| cd7e9b86b4 | |||
| c6c643ecf1 | |||
| ef15d0d576 | |||
| 5660aecb50 | |||
| 6a66dd803b | |||
| b42f0722d7 | |||
| 45c8bb83be | |||
| 6469e3bc97 | |||
| 10f98e9c99 | |||
| e5b6f650b2 | |||
| 3b2ed3f0f2 | |||
| 20e102ec5c | |||
| 5a003945ac | |||
| 29aeb046de | |||
| 72941da1cb | |||
| 81d2197a3e | |||
| 9dd9304a71 | |||
| 0c87906cc3 | |||
| 1227408407 | |||
| f8d7544517 | |||
| a9843ed459 | |||
| 46f9503543 | |||
| c5d0646c42 | |||
| b441658570 | |||
| e95c457e44 | |||
| 6de9d547b7 | |||
| 3a93c9c078 | |||
| 36d01a0a89 | |||
| 6434b4dfce | |||
| e964a8ed44 | |||
| c39a59e54e | |||
| 34c852b12a | |||
| 7b00b19fce | |||
| ad446500f9 | |||
| d490247d1e | |||
| dee36395a6 | |||
| 6df195b6ec | |||
| 946efef252 | |||
| 73b1114883 | |||
| 1b5465bea8 | |||
| 5288c701c1 | |||
| 10af1ba341 | |||
| 26dff4f418 | |||
| b5f3dec93b | |||
| a82fc0fb9e | |||
| e2ec0a3b64 | |||
| f4668b6e81 | |||
| d5281348b6 | |||
| 1717df113e | |||
| 0355bdbbab | |||
| 02677de5c0 | |||
| 886c1092d6 | |||
| 191569792c | |||
| da1fe34249 | |||
| 4ee807973e | |||
| c5f7cb2c68 | |||
| 88a7089289 | |||
| b89f7ac1b4 | |||
| 8079769881 | |||
| 2274b476d6 | |||
| e12aedd1aa | |||
| 434aaf6136 | |||
| d8b6669d12 | |||
| dd3d93edc7 | |||
| 7bc603028d | |||
| c18012f65a | |||
| b15967ff31 | |||
| 2db6510a8a | |||
| 1837336865 | |||
| eab0e634a2 | |||
| 8870ebdb5e | |||
| 9df9d9ae80 | |||
| 67ba489fe2 | |||
| da9a359251 | |||
| 0661729e5f | |||
| ddafd90d3e | |||
| 8960aa5545 | |||
| a0c2b5ade8 | |||
| a1acd3519f | |||
| c3d008ec0f | |||
| 8ae53f1c49 | |||
| 179c2a5157 | |||
| dd7e5dae36 | |||
| e165f01930 | |||
| 940d62cde4 | |||
| b002cf2df1 | |||
| 56c73c2555 | |||
| 28fb9834e1 | |||
| 6b4b16c13b | |||
| d743f7ee12 | |||
| a4e8311cbd | |||
| c172aa8bf8 | |||
| d1926fe372 | |||
| 2b658ac381 | |||
| 321d291b4b | |||
| 2eb26e4e38 | |||
| 3b06d1a6ef | |||
| de824375d3 | |||
| 11af9c02d9 | |||
| 09e429fc67 | |||
| 703b4f89a6 | |||
| 32e054eb84 | |||
| 5e368552ea | |||
| 0379786cbd | |||
| a9a5eb6735 | |||
| ab70f7e498 | |||
| 1407fe36f3 | |||
| d12801e34d | |||
| 3e7190e279 | |||
| 41423feffe | |||
| 30b585c0c1 | |||
| a3c93f0d39 | |||
| f53894b16a | |||
| 7533c349ef | |||
| 91569ced40 | |||
| f9ae778b21 | |||
| 427dfaafab | |||
| ae589aeb54 | |||
| 1b9d2969eb | |||
| daffbcde72 | |||
| 9445c6f21e | |||
| 6febb99499 | |||
| 6e6979cfe3 | |||
| 230cdb0e37 | |||
| ce450e9b6d | |||
| de36a24191 | |||
| b167ba07f7 | |||
| 4d40225a44 | |||
| 57b9c2babc | |||
| 9dc9ce37d8 | |||
| f245840cde | |||
| 4824547dde | |||
| 8dbee32eee | |||
| ae7c5ff0c3 | |||
| 2a465f88c5 | |||
| 58ae9b589a | |||
| 8bc01d3f24 | |||
| d0df5dd641 | |||
| 2cd15d25e9 | |||
| dafac06bc8 | |||
| e2651728c5 | |||
| 673dea2e57 | |||
| 7fbe649dc9 | |||
| 3766899c83 | |||
| a6c7d54fe7 | |||
| 79bc04bec1 | |||
| f9834b5f4d | |||
| fc7b8f4c16 | |||
| 4f6e81677c | |||
| 6b7ecd3044 | |||
| 8ef5f90abd | |||
| a334adffc6 | |||
| f1db883609 | |||
| e586a11e2a | |||
| 50b893f537 | |||
| 02efb9a8e5 | |||
| 38b9a772cd | |||
| 618430433d | |||
| 84cd398c09 | |||
| 385a9bba73 | |||
| 8218a452bd | |||
| a77e2eb3ad | |||
| d1a0bed00e | |||
| 66d4770858 | |||
| 80c5f9b84d | |||
| 79f46cb745 | |||
| de32a9862d | |||
| 0e119e4834 | |||
| 29c8e00477 | |||
| dc6ad9cdd3 | |||
| dcd754dac8 | |||
| d88fb18319 | |||
| 420e9c4662 | |||
| 98d6a1cc64 | |||
| 09ad081b37 | |||
| aa0fd9cafd | |||
| bae8290273 | |||
| 1b799a6973 | |||
| ed3b55a1e2 | |||
| 97c01ce81a | |||
| e96637219f | |||
| 17244b0006 | |||
| 67a02f06da | |||
| 6b6f345618 | |||
| 2ac9d3e977 | |||
| 93692ec255 | |||
| 99852f591e | |||
| b89525746d | |||
| c05834f2a1 | |||
| 9bbfb4763d | |||
| 22e6070e53 | |||
| ba218c85e0 | |||
| 644d2b06ac | |||
| 8d4c8a4553 | |||
| 077174a9a2 | |||
| ce31b95fb7 | |||
| 881eedbf3a | |||
| 09cb6f7b2b | |||
| bd091d5cb9 | |||
| 8cb67a8d20 | |||
| 290bb29e64 | |||
| d0769a5e37 | |||
| c5b28df2ae | |||
| c108fa509f | |||
| 1e5e9801be | |||
| 09b16c980b | |||
| 4c26fc808e | |||
| 525b11b346 | |||
| 86679b498b | |||
| 46df8b0528 | |||
| 1a4f896a8a | |||
| aaaa15a0ef | |||
| de65b1c699 | |||
| f9437065ee | |||
| b495cadae9 | |||
| 47995b77f7 | |||
| bc24ec5272 | |||
| 2947c41a72 | |||
| ef53035f70 | |||
| 290afc3f8f | |||
| d6e89b0880 | |||
| 2b72552b1f | |||
| df69418855 | |||
| 472e402521 | |||
| a3f282667c | |||
| b86263d972 | |||
| f278320b93 | |||
| 6345666ae6 | |||
| 7b5ebab453 | |||
| d4d713b12d | |||
| ab3af54e15 | |||
| b01e1eb8a1 | |||
| 0724932152 | |||
| cd7b15aadf | |||
| 37fc167002 | |||
| 9feeb302e8 | |||
| bba35d189e | |||
| cd5e4bbd60 | |||
| a513bf13ca | |||
| e3e570e664 | |||
| badff85e28 | |||
| 4a0f75044f | |||
| b729a7cead | |||
| 4375ca92d3 | |||
| 71537b283f | |||
| 63506dac1c | |||
| e716fae1c5 | |||
| f7370bc802 | |||
| 72c3fc78b3 | |||
| 110387dbd3 | |||
| 2820f151e8 | |||
| 9517df5082 | |||
| 56cedf0144 | |||
| bbaee7cd4d | |||
| 8ee2bdf488 | |||
| 97ecc83fe4 | |||
| 57f62a6087 | |||
| 2e760ff461 | |||
| 0df26cbd54 | |||
| 5f1ab4a2f3 | |||
| e1ff8c03e1 | |||
| 55f72c35a6 | |||
| 6c53701a59 | |||
| 02bb634257 | |||
| 5581c03f77 | |||
| cf788fe07b | |||
| 4bf425e1ca | |||
| a2f4fd5d9b | |||
| 295a1524d8 | |||
| 234154255c | |||
| 7b087840ec | |||
| 16b594ebdd | |||
| 67b3101fd1 | |||
| b3ce56c605 | |||
| 28cefa792c | |||
| 0803abc168 | |||
| 02ae883fa4 | |||
| be4050768e | |||
| dc6ec23cb9 | |||
| 1bb98c13d1 | |||
| bca979bab5 | |||
| e4fafd764c | |||
| 172159414b | |||
| 9355138a8c | |||
| 343cd8b772 | |||
| 01e0d5b94d | |||
| ac00667465 | |||
| 3deae2bfeb | |||
| 3f7b0f6563 | |||
| e6b9d4f273 | |||
| a00231dd3c | |||
| 3bc172e7e0 | |||
| ee9df21ae5 | |||
| f96b256ad3 | |||
| f2c50e929e | |||
| 02e3239848 | |||
| 8a54b027d0 | |||
| 3b11e896d4 | |||
| 89926b2c31 | |||
| 7b4e89555e | |||
| 1e37186247 | |||
| 154c763719 | |||
| 80197d5834 | |||
| 7e95103a2d | |||
| efe1a1f543 | |||
| 4fea690670 | |||
| f1dee1061d | |||
| 61cf0fc08d | |||
| 0c86e5dae1 | |||
| 638898fa28 | |||
| e7cd68e1c8 | |||
| e40e6faebd | |||
| 3d07aac944 | |||
| 1a5493facf | |||
| 9013b9492c | |||
| 188f26ad65 | |||
| 3ceb5a0c0f | |||
| e1ce052d3c | |||
| 70a379edef | |||
| 35ea3154d1 | |||
| ebf66821a2 | |||
| 8463bee253 | |||
| 860680d001 | |||
| df39166279 | |||
| 32fda46f0a | |||
| 36ecae7e6e | |||
| a5bfe4e3d5 | |||
| 4faeddc3f3 | |||
| 98f7bf366f | |||
| af3a9e5ce2 | |||
| 52eb7b1afe | |||
| 490fbd241d | |||
| f132131156 | |||
| c1e680a063 | |||
| c66b06c2c9 | |||
| 65e605cdc4 | |||
| d2fdb4efd9 | |||
| d0deb9d647 | |||
| 5495c90eaf | |||
| bf3ffae67c | |||
| aa0337ea33 | |||
| 4991d735bf | |||
| 398e61bddb | |||
| e6576f4a54 | |||
| c3b9e135b0 | |||
| 3bd4948c43 | |||
| f3cd1380be | |||
| a2c3dfbf85 | |||
| 3c37aafe1f | |||
| c591c182b3 | |||
| 9cc50078d1 | |||
| 7728759bcd | |||
| ce8fed350e | |||
| a005945e9e | |||
| cf86520fae | |||
| db6fdf6baf | |||
| 975ad50afc | |||
| 0c27df7754 | |||
| 102a860ba3 | |||
| 3a886714a0 | |||
| 09ab638239 | |||
| a4f88c78f4 | |||
| ccf2a3b617 | |||
| c8f941a779 | |||
| 5510cbb8e9 | |||
| a434173b54 | |||
| 7387f700fb | |||
| 4f01baaa23 | |||
| 09b37f0ff2 | |||
| 324d5709e3 | |||
| 3f23e4f1f1 | |||
| 9776a35f9f | |||
| 9b9ee70288 | |||
| 2628f69651 | |||
| b9c0a32862 | |||
| 82644a2ff4 | |||
| 3d2c93b5ac | |||
| c447114297 | |||
| 857de9ffcc | |||
| eea656bd7b | |||
| eec5284306 | |||
| 88a6a768c4 | |||
| edac1a224c | |||
| e67d1c5697 | |||
| 30502ec949 | |||
| a2c3913601 | |||
| f1c7713da2 | |||
| d6a41d5a82 | |||
| 72b5ca4153 | |||
| aeec2e1c32 | |||
| f9889bea3d | |||
| 2cad2ac2e9 | |||
| d948fe2631 | |||
| 2b5525323b | |||
| 58156e0d61 | |||
| a4b0dfe43e | |||
| ee2433a5ae | |||
| 2151b8502d | |||
| b57fde9b0a | |||
| 86706f9422 | |||
| 0687f268fc | |||
| bc426831db | |||
| 276e553e13 | |||
| e7ab302c61 | |||
| a5d70ce4b5 | |||
| d67be313e6 | |||
| 15d2d029dc | |||
| b6ea5e6549 | |||
| f378b0651a | |||
| 1a0573e0d0 | |||
| 9f103d8df1 | |||
| daa899a1ef | |||
| 59cb72a11d | |||
| 28c1b6d31d | |||
| dcb791c9a2 | |||
| 907259bf73 | |||
| 02f7ddbb37 | |||
| 63b1ca9b56 | |||
| 39857cf6e6 | |||
| 3090ae69f3 | |||
| 92186a86cc | |||
| 97e8470b0d | |||
| 6b0e3503a7 | |||
| 1e2de7656e | |||
| 56c6a7efb0 | |||
| 9c4e54fc6e | |||
| 2c47436259 | |||
| 9b5d16ae92 | |||
| deb13674b2 | |||
| 17c82ff409 | |||
| f9e314bf9f | |||
| e4c1930dd1 | |||
| b337ab424d | |||
| 82a0e194cb | |||
| 599296c4e3 | |||
| 2594a607dc | |||
| 335d4e24da | |||
| becc277123 | |||
| 52cdd41ec8 | |||
| 53548ba7a6 | |||
| 1dc438beb2 | |||
| c9ba69792f | |||
| ab67e5f4aa | |||
| 557608e318 | |||
| a83fedc9b8 | |||
| 61a17b198f | |||
| 3df1db4ad8 | |||
| e46cfa0d77 | |||
| 4126d31a5e | |||
| 9d9549cdd4 | |||
| eb40de6eb4 | |||
| 6efd09db73 | |||
| 3f09e3d387 | |||
| 05868e0e00 | |||
| 580a73f9a5 | |||
| ab7110d49f | |||
| 875781335c | |||
| 625340cf8a | |||
| 8d9dbc3957 | |||
| 07d813082b | |||
| a684f60252 | |||
| 931cae3c98 | |||
| dfd82a6293 | |||
| 82d4b11de3 | |||
| 75473937cf | |||
| a68bbab8ab | |||
| 5cfd2c9a52 | |||
| 6c7b31d76c | |||
| 2924ac2900 | |||
| a501625dd6 | |||
| cc64ce4498 | |||
| c9378e6cae | |||
| 62c7f26540 | |||
| 18e3ef9a79 | |||
| 395b0101a8 | |||
| 84a0bd2cd9 | |||
| 9cd181c5b8 | |||
| 41828a6e41 | |||
| 356e398caf | |||
| 6cb978df98 | |||
| 4cb0efa6bd | |||
| e0fa58da57 | |||
| 5d31d8d1a2 | |||
| 53a01ad977 | |||
| d7791756dc | |||
| dd48ee2f7e | |||
| ec64ec3d63 | |||
| 35dbfeb5e7 | |||
| a9ecfccfd2 | |||
| d850650aeb | |||
| 43e256f38c | |||
| b5f9cf201d | |||
| 6e121a3ce2 | |||
| 555e37eaf7 | |||
| 9675e79441 | |||
| 345851bf1d | |||
| 7c5a3893ef | |||
| b53b5cf91f | |||
| 04813173e4 | |||
| c4270b0839 | |||
| bb24ed53a4 | |||
| 1b74b21420 | |||
| b7cbe2a0b4 | |||
| 500b94b44a | |||
| 641466a731 | |||
| e3ea83bb47 | |||
| b6043744a9 | |||
| 2b38044271 | |||
| 4c3d2643c1 | |||
| e2cc0c0b80 | |||
| ce55dce011 | |||
| a738c19316 | |||
| 63b8176bdf | |||
| 6a762f570d | |||
| 1b7424f750 | |||
| bdd4f705be | 
							
								
								
									
										113
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								.drone.yml
									
									
									
									
									
								
							@@ -1,5 +1,27 @@
 | 
			
		||||
---
 | 
			
		||||
kind: secret
 | 
			
		||||
name: docker_username
 | 
			
		||||
get:
 | 
			
		||||
  path: odit-registry-builder
 | 
			
		||||
  name: username
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
kind: secret
 | 
			
		||||
name: docker_password
 | 
			
		||||
get:
 | 
			
		||||
  path: odit-registry-builder
 | 
			
		||||
  name: password
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
kind: secret
 | 
			
		||||
name: git_ssh
 | 
			
		||||
get:
 | 
			
		||||
  path: odit-git-bot
 | 
			
		||||
  name: sshkey
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
kind: pipeline
 | 
			
		||||
type: kubernetes
 | 
			
		||||
name: tests:node_latest
 | 
			
		||||
clone:
 | 
			
		||||
  disable: true
 | 
			
		||||
@@ -9,9 +31,8 @@ steps:
 | 
			
		||||
    commands:
 | 
			
		||||
      - git clone $DRONE_REMOTE_URL .
 | 
			
		||||
      - git checkout $DRONE_SOURCE_BRANCH
 | 
			
		||||
      - mv .env.ci .env
 | 
			
		||||
  - name: run tests
 | 
			
		||||
    image: node:alpine
 | 
			
		||||
    image: node:latest
 | 
			
		||||
    commands:
 | 
			
		||||
      - yarn
 | 
			
		||||
      - yarn test:ci
 | 
			
		||||
@@ -21,27 +42,65 @@ trigger:
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
kind: pipeline
 | 
			
		||||
type: docker
 | 
			
		||||
type: kubernetes
 | 
			
		||||
name: build:dev
 | 
			
		||||
clone:
 | 
			
		||||
  disable: true
 | 
			
		||||
 | 
			
		||||
steps:
 | 
			
		||||
  - name: clone
 | 
			
		||||
    image: alpine/git
 | 
			
		||||
    commands:
 | 
			
		||||
      - git clone $DRONE_REMOTE_URL .
 | 
			
		||||
      - git checkout dev
 | 
			
		||||
  - name: build dev
 | 
			
		||||
    image: plugins/docker
 | 
			
		||||
    depends_on: [clone]
 | 
			
		||||
    settings:
 | 
			
		||||
      username:
 | 
			
		||||
        from_secret: DOCKER_REGISTRY_USER
 | 
			
		||||
        from_secret: docker_username
 | 
			
		||||
      password:
 | 
			
		||||
        from_secret: DOCKER_REGISTRY_PASSWORD
 | 
			
		||||
        from_secret: docker_password
 | 
			
		||||
      repo: registry.odit.services/lfk/backend
 | 
			
		||||
      tags:
 | 
			
		||||
        - dev
 | 
			
		||||
      registry: registry.odit.services
 | 
			
		||||
    when:
 | 
			
		||||
      branch:
 | 
			
		||||
        - dev
 | 
			
		||||
      event:
 | 
			
		||||
        - push
 | 
			
		||||
      mtu: 1000
 | 
			
		||||
  - name: run changelog export
 | 
			
		||||
    depends_on: ["clone"]
 | 
			
		||||
    image: node:latest
 | 
			
		||||
    commands:
 | 
			
		||||
      - npx auto-changelog --commit-limit false -p -u --hide-credit
 | 
			
		||||
  - name: push new changelog to repo
 | 
			
		||||
    depends_on: ["run changelog export"]
 | 
			
		||||
    image: appleboy/drone-git-push
 | 
			
		||||
    settings:
 | 
			
		||||
      branch: dev
 | 
			
		||||
      commit: true
 | 
			
		||||
      commit_message: 🧾New changelog file version [CI SKIP] [skip ci]
 | 
			
		||||
      author_email: bot@odit.services
 | 
			
		||||
      remote: git@git.odit.services:lfk/backend.git
 | 
			
		||||
      ssh_key:
 | 
			
		||||
        from_secret: git_ssh
 | 
			
		||||
  - name: run full license export
 | 
			
		||||
    depends_on: ["clone"]
 | 
			
		||||
    image: node:14.15.1-alpine3.12
 | 
			
		||||
    commands:
 | 
			
		||||
      - yarn
 | 
			
		||||
      - yarn licenses:export
 | 
			
		||||
  - name: push new licenses file to repo
 | 
			
		||||
    depends_on: ["run full license export"]
 | 
			
		||||
    image: appleboy/drone-git-push
 | 
			
		||||
    settings:
 | 
			
		||||
      branch: dev
 | 
			
		||||
      commit: true
 | 
			
		||||
      commit_message: 📖New license file version [CI SKIP] [skip ci]
 | 
			
		||||
      author_email: bot@odit.services
 | 
			
		||||
      remote: git@git.odit.services:lfk/backend.git
 | 
			
		||||
      skip_verify: true
 | 
			
		||||
      ssh_key:
 | 
			
		||||
        from_secret: git_ssh
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
trigger:
 | 
			
		||||
  branch:
 | 
			
		||||
@@ -51,22 +110,41 @@ trigger:
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
kind: pipeline
 | 
			
		||||
type: docker
 | 
			
		||||
type: kubernetes
 | 
			
		||||
name: build:latest
 | 
			
		||||
clone:
 | 
			
		||||
  disable: true
 | 
			
		||||
 | 
			
		||||
steps:
 | 
			
		||||
  - name: clone
 | 
			
		||||
    image: alpine/git
 | 
			
		||||
    commands:
 | 
			
		||||
      - git clone $DRONE_REMOTE_URL .
 | 
			
		||||
      - git checkout dev
 | 
			
		||||
      - git merge main
 | 
			
		||||
      - git checkout main
 | 
			
		||||
  - name: build latest
 | 
			
		||||
    depends_on: ["clone"]
 | 
			
		||||
    image: plugins/docker
 | 
			
		||||
    depends_on: [clone]
 | 
			
		||||
    settings:
 | 
			
		||||
      username:
 | 
			
		||||
        from_secret: DOCKER_REGISTRY_USER
 | 
			
		||||
        from_secret: docker_username
 | 
			
		||||
      password:
 | 
			
		||||
        from_secret: DOCKER_REGISTRY_PASSWORD
 | 
			
		||||
        from_secret: docker_password
 | 
			
		||||
      repo: registry.odit.services/lfk/backend
 | 
			
		||||
      tags:
 | 
			
		||||
        - latest
 | 
			
		||||
      registry: registry.odit.services
 | 
			
		||||
      mtu: 1000
 | 
			
		||||
  - name: push merge to repo
 | 
			
		||||
    depends_on: ["clone"]
 | 
			
		||||
    image: appleboy/drone-git-push
 | 
			
		||||
    settings:
 | 
			
		||||
      branch: dev
 | 
			
		||||
      commit: false
 | 
			
		||||
      remote: git@git.odit.services:lfk/backend.git
 | 
			
		||||
      ssh_key:
 | 
			
		||||
        from_secret: git_ssh
 | 
			
		||||
 | 
			
		||||
trigger:
 | 
			
		||||
  branch:
 | 
			
		||||
@@ -76,7 +154,7 @@ trigger:
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
kind: pipeline
 | 
			
		||||
type: docker
 | 
			
		||||
type: kubernetes
 | 
			
		||||
name: build:tags
 | 
			
		||||
 | 
			
		||||
steps:
 | 
			
		||||
@@ -85,13 +163,14 @@ steps:
 | 
			
		||||
    depends_on: [clone]
 | 
			
		||||
    settings:
 | 
			
		||||
      username:
 | 
			
		||||
        from_secret: DOCKER_REGISTRY_USER
 | 
			
		||||
        from_secret: docker_username
 | 
			
		||||
      password:
 | 
			
		||||
        from_secret: DOCKER_REGISTRY_PASSWORD
 | 
			
		||||
        from_secret: docker_password
 | 
			
		||||
      repo: registry.odit.services/lfk/backend
 | 
			
		||||
      tags:
 | 
			
		||||
        - '${DRONE_TAG}'
 | 
			
		||||
      registry: registry.odit.services
 | 
			
		||||
      mtu: 1000
 | 
			
		||||
  - name: trigger node lib build
 | 
			
		||||
    image: idcooldi/drone-webhook
 | 
			
		||||
    settings:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.env.ci
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								.env.ci
									
									
									
									
									
								
							@@ -6,4 +6,4 @@ DB_USER=unused
 | 
			
		||||
DB_PASSWORD=bla
 | 
			
		||||
DB_NAME=./test.sqlite
 | 
			
		||||
NODE_ENV=dev
 | 
			
		||||
POSTALCODE_COUNTRYCODE=null
 | 
			
		||||
POSTALCODE_COUNTRYCODE=DE
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
APP_PORT=4010
 | 
			
		||||
DB_TYPE=bla
 | 
			
		||||
DB_TYPE=sqlite
 | 
			
		||||
DB_HOST=bla
 | 
			
		||||
DB_PORT=bla
 | 
			
		||||
DB_USER=bla
 | 
			
		||||
DB_PASSWORD=bla
 | 
			
		||||
DB_NAME=bla
 | 
			
		||||
DB_NAME=./test.sqlite
 | 
			
		||||
NODE_ENV=production
 | 
			
		||||
POSTALCODE_COUNTRYCODE=null
 | 
			
		||||
POSTALCODE_COUNTRYCODE=DE
 | 
			
		||||
SEED_TEST_DATA=false
 | 
			
		||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -133,4 +133,6 @@ build
 | 
			
		||||
*.sqlite
 | 
			
		||||
*.sqlite-jurnal
 | 
			
		||||
/docs
 | 
			
		||||
lib
 | 
			
		||||
lib
 | 
			
		||||
/oss-attribution
 | 
			
		||||
*.tmp
 | 
			
		||||
							
								
								
									
										1546
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1546
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										101
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										101
									
								
								README.md
									
									
									
									
									
								
							@@ -2,20 +2,18 @@
 | 
			
		||||
 | 
			
		||||
Backend Server
 | 
			
		||||
 | 
			
		||||
## Quickstart 🐳
 | 
			
		||||
> Use this to run the backend with a postgresql db in docker
 | 
			
		||||
 | 
			
		||||
1. Clone the repo or copy the docker-compose 
 | 
			
		||||
2. Run in toe folder that contains the docker-compose file: `docker-compose up -d`
 | 
			
		||||
3. Visit http://127.0.0.1:4010/api/docs to check if the server is running
 | 
			
		||||
4. You can now use the default admin user (`demo:demo`)
 | 
			
		||||
 | 
			
		||||
## Dev Setup 🛠
 | 
			
		||||
> Local dev setup utilizing sqlite3 as the database.
 | 
			
		||||
 | 
			
		||||
### Local w/ sqlite
 | 
			
		||||
 | 
			
		||||
1. Create a .env file in the project root containing:
 | 
			
		||||
   ```
 | 
			
		||||
    APP_PORT=4010
 | 
			
		||||
    DB_TYPE=sqlite
 | 
			
		||||
    DB_HOST=bla
 | 
			
		||||
    DB_PORT=bla
 | 
			
		||||
    DB_USER=bla
 | 
			
		||||
    DB_PASSWORD=bla
 | 
			
		||||
    DB_NAME=./test.sqlite
 | 
			
		||||
   ```
 | 
			
		||||
1. Rename the .env.example file to .env (you can adjust app port and other settings, if needed)
 | 
			
		||||
2. Install Dependencies
 | 
			
		||||
   ```bash
 | 
			
		||||
   yarn
 | 
			
		||||
@@ -25,16 +23,52 @@ Backend Server
 | 
			
		||||
   yarn dev
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
### Generate Docs
 | 
			
		||||
### Run Tests
 | 
			
		||||
```bash
 | 
			
		||||
# Run tests once (server has to run)
 | 
			
		||||
yarn test
 | 
			
		||||
 | 
			
		||||
# Run test in watch mode (reruns on change)
 | 
			
		||||
yarn test:watch
 | 
			
		||||
 | 
			
		||||
# Run test in ci mode (automaticly starts the dev server)
 | 
			
		||||
yarn test:ci
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Use your own mail templates
 | 
			
		||||
> You use your own mail templates by replacing the default ones we provided (either in-code or by mounting them into the /app/static/mail_templates folder).
 | 
			
		||||
 | 
			
		||||
The mail templates always come in a .html and a .txt variant to provide compatability with legacy mail clients.
 | 
			
		||||
Currently the following templates exist:
 | 
			
		||||
* pw-reset.(html/txt)
 | 
			
		||||
 | 
			
		||||
### Generate Docs
 | 
			
		||||
```bash
 | 
			
		||||
yarn docs
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Docker w/ postgres 🐳
 | 
			
		||||
## ENV Vars
 | 
			
		||||
> You can provide them via .env file or docker env vars.
 | 
			
		||||
> You can use the `test:ci:generate_env` package script to generate a example env (uses bs data as test server and ignores the errors).
 | 
			
		||||
 | 
			
		||||
| Name | Type | Default | Description
 | 
			
		||||
| - | - | - | -
 | 
			
		||||
| APP_PORT | Number | 4010 | The port the backend server listens on. Is optional.
 | 
			
		||||
| DB_TYPE | String | N/A | The type of the db u want to use. It has to be supported by typeorm. Possible: `sqlite`, `mysql`, `postgresql`
 | 
			
		||||
| DB_HOST | String | N/A | The db's host's ip-address/fqdn or file path for sqlite
 | 
			
		||||
| DB_PORT | String | N/A | The db's port
 | 
			
		||||
| DB_USER | String | N/A | The user for accessing the db
 | 
			
		||||
| DB_PASSWORD | String | N/A | The user's password for accessing the db
 | 
			
		||||
| DB_NAME | String | N/A | The db's name
 | 
			
		||||
| NODE_ENV | String | dev | The apps env - influences debug info. Also when the env is set to "test", mailing errors get ignored.
 | 
			
		||||
| POSTALCODE_COUNTRYCODE | String/CountryCode | N/A | The countrycode used to validate address's postal codes
 | 
			
		||||
| PHONE_COUNTRYCODE | String/CountryCode | null (international) | The countrycode used to validate phone numers
 | 
			
		||||
| SEED_TEST_DATA | Boolean | False | If you want the app to seed some example data set this to true
 | 
			
		||||
| MAILER_URL | String(Url) | N/A | The mailer's base url (no trailing slash)
 | 
			
		||||
| MAILER_KEY | String | N/A | The mailer's api key.
 | 
			
		||||
| IMPRINT_URL | String(Url) | /imprint | The link to a imprint page for the system (Defaults to the frontend's imprint)
 | 
			
		||||
| PRIVACY_URL | String(Url) | /privacy | The link to a privacy page for the system (Defaults to the frontend's privacy page)
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
docker-compose up --build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Recommended Editor
 | 
			
		||||
 | 
			
		||||
@@ -42,22 +76,19 @@ docker-compose up --build
 | 
			
		||||
 | 
			
		||||
### Recommended Extensions
 | 
			
		||||
 | 
			
		||||
- will be automatically recommended via ./vscode/extensions.json
 | 
			
		||||
* will be automatically recommended via ./vscode/extensions.json
 | 
			
		||||
 | 
			
		||||
## Branches
 | 
			
		||||
- main: Protected "release" branch
 | 
			
		||||
- dev: Current dev branch for merging the different features - only push for merges or minor changes!
 | 
			
		||||
- feature/xyz: Feature branches - `feature/issueid-title`
 | 
			
		||||
- bugfix/xyz: Branches for bugfixes - `bugfix/issueid-title` (no id for readme changes needed)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## File Structure
 | 
			
		||||
 | 
			
		||||
- src/models/entities\* - database models (typeorm entities)
 | 
			
		||||
- src/models/actions\* - actions models
 | 
			
		||||
- src/models/responses\* - response models
 | 
			
		||||
- src/controllers/\* - routing-controllers
 | 
			
		||||
- src/loaders/\* - loaders for the different init steps of the api server
 | 
			
		||||
- src/middlewares/\* - express middlewares (mainly auth r/n)
 | 
			
		||||
- src/errors/* - our custom (http) errors
 | 
			
		||||
- src/routes/\* - express routes for everything we don't do via routing-controllers (depreciated)
 | 
			
		||||
## Staging
 | 
			
		||||
### Branches & Tags
 | 
			
		||||
* vX.Y.Z: Release tags created from the main branch
 | 
			
		||||
   * The version numbers follow the semver standard
 | 
			
		||||
   * A new release tag automaticly triggers the release ci pipeline
 | 
			
		||||
* main: Protected "release" branch
 | 
			
		||||
   * The latest tag of the docker image get's build from this
 | 
			
		||||
   * New releases get created as tags from this   
 | 
			
		||||
* dev: Current dev branch for merging the different feature branches and bugfixes
 | 
			
		||||
   * The dev tag of the docker image get's build from this
 | 
			
		||||
   * Only push minor changes to this branch!
 | 
			
		||||
   * To merge a feature branch into this please create a pull request
 | 
			
		||||
* feature/xyz: Feature branches - nameing scheme: `feature/issueid-title`
 | 
			
		||||
* bugfix/xyz: Branches for bugfixes - nameing scheme:`bugfix/issueid-title`
 | 
			
		||||
							
								
								
									
										1404
									
								
								licenses.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1404
									
								
								licenses.md
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										71
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										71
									
								
								package.json
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@odit/lfk-backend",
 | 
			
		||||
  "version": "0.0.5",
 | 
			
		||||
  "version": "0.10.1",
 | 
			
		||||
  "main": "src/app.ts",
 | 
			
		||||
  "repository": "https://git.odit.services/lfk/backend",
 | 
			
		||||
  "author": {
 | 
			
		||||
@@ -22,11 +22,13 @@
 | 
			
		||||
  ],
 | 
			
		||||
  "license": "CC-BY-NC-SA-4.0",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "argon2": "^0.27.0",
 | 
			
		||||
    "@odit/class-validator-jsonschema": "2.1.1",
 | 
			
		||||
    "argon2": "^0.27.1",
 | 
			
		||||
    "axios": "^0.21.1",
 | 
			
		||||
    "body-parser": "^1.19.0",
 | 
			
		||||
    "class-transformer": "^0.3.1",
 | 
			
		||||
    "class-validator": "^0.12.2",
 | 
			
		||||
    "class-validator-jsonschema": "^2.0.3",
 | 
			
		||||
    "check-password-strength": "^2.0.2",
 | 
			
		||||
    "class-transformer": "0.3.1",
 | 
			
		||||
    "class-validator": "^0.13.1",
 | 
			
		||||
    "consola": "^2.15.0",
 | 
			
		||||
    "cookie": "^0.4.1",
 | 
			
		||||
    "cookie-parser": "^1.4.5",
 | 
			
		||||
@@ -35,36 +37,38 @@
 | 
			
		||||
    "dotenv": "^8.2.0",
 | 
			
		||||
    "express": "^4.17.1",
 | 
			
		||||
    "jsonwebtoken": "^8.5.1",
 | 
			
		||||
    "libphonenumber-js": "^1.9.9",
 | 
			
		||||
    "mysql": "^2.18.1",
 | 
			
		||||
    "pg": "^8.5.1",
 | 
			
		||||
    "reflect-metadata": "^0.1.13",
 | 
			
		||||
    "routing-controllers": "^0.9.0-alpha.6",
 | 
			
		||||
    "routing-controllers-openapi": "^2.1.0",
 | 
			
		||||
    "sqlite3": "^5.0.0",
 | 
			
		||||
    "typeorm": "^0.2.29",
 | 
			
		||||
    "routing-controllers": "0.9.0-alpha.6",
 | 
			
		||||
    "routing-controllers-openapi": "^2.2.0",
 | 
			
		||||
    "sqlite3": "5.0.0",
 | 
			
		||||
    "typeorm": "^0.2.30",
 | 
			
		||||
    "typeorm-routing-controllers-extensions": "^0.2.0",
 | 
			
		||||
    "typeorm-seeding": "^1.6.1",
 | 
			
		||||
    "uuid": "^8.3.1",
 | 
			
		||||
    "uuid": "^8.3.2",
 | 
			
		||||
    "validator": "^13.5.2"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/cors": "^2.8.8",
 | 
			
		||||
    "@odit/license-exporter": "^0.0.9",
 | 
			
		||||
    "@types/cors": "^2.8.9",
 | 
			
		||||
    "@types/csvtojson": "^1.1.5",
 | 
			
		||||
    "@types/express": "^4.17.9",
 | 
			
		||||
    "@types/jest": "^26.0.16",
 | 
			
		||||
    "@types/express": "^4.17.11",
 | 
			
		||||
    "@types/jest": "^26.0.20",
 | 
			
		||||
    "@types/jsonwebtoken": "^8.5.0",
 | 
			
		||||
    "@types/node": "^14.14.9",
 | 
			
		||||
    "@types/node": "^14.14.22",
 | 
			
		||||
    "@types/uuid": "^8.3.0",
 | 
			
		||||
    "axios": "^0.21.0",
 | 
			
		||||
    "cp-cli": "^2.0.0",
 | 
			
		||||
    "jest": "^26.6.3",
 | 
			
		||||
    "nodemon": "^2.0.6",
 | 
			
		||||
    "rimraf": "^2.7.1",
 | 
			
		||||
    "start-server-and-test": "^1.11.6",
 | 
			
		||||
    "ts-jest": "^26.4.4",
 | 
			
		||||
    "ts-node": "^9.0.0",
 | 
			
		||||
    "typedoc": "^0.19.2",
 | 
			
		||||
    "typescript": "^4.1.2"
 | 
			
		||||
    "nodemon": "^2.0.7",
 | 
			
		||||
    "release-it": "^14.2.2",
 | 
			
		||||
    "rimraf": "^3.0.2",
 | 
			
		||||
    "start-server-and-test": "^1.11.7",
 | 
			
		||||
    "ts-jest": "^26.5.0",
 | 
			
		||||
    "ts-node": "^9.1.1",
 | 
			
		||||
    "typedoc": "^0.20.19",
 | 
			
		||||
    "typescript": "^4.1.3"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "nodemon src/app.ts",
 | 
			
		||||
@@ -72,9 +76,26 @@
 | 
			
		||||
    "docs": "typedoc --out docs src",
 | 
			
		||||
    "test": "jest",
 | 
			
		||||
    "test:watch": "jest --watchAll",
 | 
			
		||||
    "test:ci": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test",
 | 
			
		||||
    "test:ci:generate_env": "ts-node scripts/create_testenv.ts",
 | 
			
		||||
    "test:ci:run": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test",
 | 
			
		||||
    "test:ci": "npm run test:ci:generate_env && npm run test:ci:run",
 | 
			
		||||
    "seed": "ts-node ./node_modules/typeorm/cli.js schema:sync && ts-node ./node_modules/typeorm-seeding/dist/cli.js seed",
 | 
			
		||||
    "openapi:export": "ts-node src/openapi_export.ts"
 | 
			
		||||
    "openapi:export": "ts-node scripts/openapi_export.ts",
 | 
			
		||||
    "licenses:export": "license-exporter --markdown",
 | 
			
		||||
    "release": "release-it --only-version"
 | 
			
		||||
  },
 | 
			
		||||
  "release-it": {
 | 
			
		||||
    "git": {
 | 
			
		||||
      "commit": true,
 | 
			
		||||
      "requireCleanWorkingDir": false,
 | 
			
		||||
      "commitMessage": "🚀Bumped version to v${version}",
 | 
			
		||||
      "requireBranch": "dev",
 | 
			
		||||
      "push": false,
 | 
			
		||||
      "tag": false
 | 
			
		||||
    },
 | 
			
		||||
    "npm": {
 | 
			
		||||
      "publish": false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "nodemonConfig": {
 | 
			
		||||
    "ignore": [
 | 
			
		||||
@@ -82,4 +103,4 @@
 | 
			
		||||
      "docs/*"
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								scripts/create_testenv.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								scripts/create_testenv.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
import consola from "consola";
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const env = `
 | 
			
		||||
APP_PORT=4010
 | 
			
		||||
DB_TYPE=sqlite
 | 
			
		||||
DB_HOST=bla
 | 
			
		||||
DB_PORT=bla
 | 
			
		||||
DB_USER=bla
 | 
			
		||||
DB_PASSWORD=bla
 | 
			
		||||
DB_NAME=./test.sqlite
 | 
			
		||||
NODE_ENV=test
 | 
			
		||||
POSTALCODE_COUNTRYCODE=DE
 | 
			
		||||
SEED_TEST_DATA=true
 | 
			
		||||
MAILER_URL=https://dev.lauf-fuer-kaya.de/mailer
 | 
			
		||||
MAILER_KEY=asdasd`;
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
    fs.writeFileSync("./.env", env, { encoding: "utf-8" });
 | 
			
		||||
    consola.success("Exported ci env to .env");
 | 
			
		||||
} catch (error) {
 | 
			
		||||
    consola.error("Couldn't export the ci env");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								scripts/openapi_export.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								scripts/openapi_export.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
import { validationMetadatasToSchemas } from '@odit/class-validator-jsonschema';
 | 
			
		||||
import consola from "consola";
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import "reflect-metadata";
 | 
			
		||||
import { createExpressServer, getMetadataArgsStorage } from "routing-controllers";
 | 
			
		||||
import { generateSpec } from '../src/apispec';
 | 
			
		||||
import { config } from '../src/config';
 | 
			
		||||
import authchecker from "../src/middlewares/authchecker";
 | 
			
		||||
import { ErrorHandler } from '../src/middlewares/ErrorHandler';
 | 
			
		||||
 | 
			
		||||
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
 | 
			
		||||
createExpressServer({
 | 
			
		||||
    authorizationChecker: authchecker,
 | 
			
		||||
    middlewares: [ErrorHandler],
 | 
			
		||||
    development: config.development,
 | 
			
		||||
    cors: true,
 | 
			
		||||
    routePrefix: "/api",
 | 
			
		||||
    controllers: [`${__dirname}/../src/controllers/*.${CONTROLLERS_FILE_EXTENSION}`],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const storage = getMetadataArgsStorage();
 | 
			
		||||
const schemas = validationMetadatasToSchemas({
 | 
			
		||||
    refPointerPrefix: "#/components/schemas/",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
//Spec creation based on the previously created schemas
 | 
			
		||||
const spec = generateSpec(storage, schemas);
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
    fs.writeFileSync("./openapi.json", JSON.stringify(spec), { encoding: "utf-8" });
 | 
			
		||||
    consola.success("Exported openapi spec to openapi.json");
 | 
			
		||||
} catch (error) {
 | 
			
		||||
    consola.error("Couldn't export the openapi spec");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								src/apispec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/apispec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
import { MetadataArgsStorage } from 'routing-controllers';
 | 
			
		||||
import { routingControllersToSpec } from 'routing-controllers-openapi';
 | 
			
		||||
import { config } from './config';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This function generates a the openapi spec from route metadata and type schemas.
 | 
			
		||||
 * @param storage MetadataArgsStorage object generated by routing-controllers.
 | 
			
		||||
 * @param schemas MetadataArgsStorage object generated by class-validator-jsonschema.
 | 
			
		||||
 */
 | 
			
		||||
export function generateSpec(storage: MetadataArgsStorage, schemas) {
 | 
			
		||||
    return routingControllersToSpec(
 | 
			
		||||
        storage,
 | 
			
		||||
        {
 | 
			
		||||
            routePrefix: "/api"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            components: {
 | 
			
		||||
                schemas,
 | 
			
		||||
                "securitySchemes": {
 | 
			
		||||
                    "AuthToken": {
 | 
			
		||||
                        "type": "http",
 | 
			
		||||
                        "scheme": "bearer",
 | 
			
		||||
                        "bearerFormat": "JWT",
 | 
			
		||||
                        description: "A JWT based access token. Use /api/auth/login or /api/auth/refresh to get one."
 | 
			
		||||
                    },
 | 
			
		||||
                    "RefreshTokenCookie": {
 | 
			
		||||
                        "type": "apiKey",
 | 
			
		||||
                        "in": "cookie",
 | 
			
		||||
                        "name": "lfk_backend__refresh_token",
 | 
			
		||||
                        description: "A cookie containing a JWT based refreh token. Attention: Doesn't work in swagger-ui. Use /api/auth/login or /api/auth/refresh to get one."
 | 
			
		||||
                    },
 | 
			
		||||
                    "StatsApiToken": {
 | 
			
		||||
                        "type": "http",
 | 
			
		||||
                        "scheme": "bearer",
 | 
			
		||||
                        description: "Api token that can be obtained by creating a new stats client (post to /api/statsclients). Only valid for obtaining stats."
 | 
			
		||||
                    },
 | 
			
		||||
                    "StationApiToken": {
 | 
			
		||||
                        "type": "http",
 | 
			
		||||
                        "scheme": "bearer",
 | 
			
		||||
                        description: "Api token that can be obtained by creating a new scan station (post to /api/stations). Only valid for creating scans."
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            info: {
 | 
			
		||||
                description: `The the backend API for the LfK! runner system. <br>[Imprint](${config.imprint_url}) & [Privacy](${config.privacy_url})`,
 | 
			
		||||
                title: "LfK! Backend API",
 | 
			
		||||
                version: config.version
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
@@ -1,14 +1,16 @@
 | 
			
		||||
import consola from "consola";
 | 
			
		||||
import "reflect-metadata";
 | 
			
		||||
import { createExpressServer } from "routing-controllers";
 | 
			
		||||
import authchecker from "./authchecker";
 | 
			
		||||
import { config, e as errors } from './config';
 | 
			
		||||
import loaders from "./loaders/index";
 | 
			
		||||
import authchecker from "./middlewares/authchecker";
 | 
			
		||||
import { ErrorHandler } from './middlewares/ErrorHandler';
 | 
			
		||||
import UserChecker from './middlewares/UserChecker';
 | 
			
		||||
 | 
			
		||||
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
 | 
			
		||||
const app = createExpressServer({
 | 
			
		||||
  authorizationChecker: authchecker,
 | 
			
		||||
  currentUserChecker: UserChecker,
 | 
			
		||||
  middlewares: [ErrorHandler],
 | 
			
		||||
  development: config.development,
 | 
			
		||||
  cors: true,
 | 
			
		||||
@@ -18,6 +20,9 @@ const app = createExpressServer({
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
  await loaders(app);
 | 
			
		||||
  if (config.testing) {
 | 
			
		||||
    consola.info("🛠[config]: Discovered testing env. Mailing errors will get ignored!")
 | 
			
		||||
  }
 | 
			
		||||
  app.listen(config.internal_port, () => {
 | 
			
		||||
    consola.success(
 | 
			
		||||
      `⚡️[server]: Server is running at http://localhost:${config.internal_port}`
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,36 @@
 | 
			
		||||
import { config as configDotenv } from 'dotenv';
 | 
			
		||||
import { CountryCode } from 'libphonenumber-js';
 | 
			
		||||
import ValidatorJS from 'validator';
 | 
			
		||||
 | 
			
		||||
configDotenv();
 | 
			
		||||
export const config = {
 | 
			
		||||
    internal_port: parseInt(process.env.APP_PORT) || 4010,
 | 
			
		||||
    development: process.env.NODE_ENV === "production",
 | 
			
		||||
    testing: process.env.NODE_ENV === "test",
 | 
			
		||||
    jwt_secret: process.env.JWT_SECRET || "secretjwtsecret",
 | 
			
		||||
    phone_validation_countrycode: process.env.PHONE_COUNTRYCODE || "ZZ",
 | 
			
		||||
    postalcode_validation_countrycode: getPostalCodeLocale()
 | 
			
		||||
    phone_validation_countrycode: getPhoneCodeLocale(),
 | 
			
		||||
    postalcode_validation_countrycode: getPostalCodeLocale(),
 | 
			
		||||
    version: process.env.VERSION || require('../package.json').version,
 | 
			
		||||
    seedTestData: getDataSeeding(),
 | 
			
		||||
    app_url: process.env.APP_URL || "http://localhost:8080",
 | 
			
		||||
    privacy_url: process.env.PRIVACY_URL || "/privacy",
 | 
			
		||||
    imprint_url: process.env.IMPRINT_URL || "/imprint",
 | 
			
		||||
    mailer_url: process.env.MAILER_URL || "",
 | 
			
		||||
    mailer_key: process.env.MAILER_KEY || ""
 | 
			
		||||
}
 | 
			
		||||
let errors = 0
 | 
			
		||||
if (typeof config.internal_port !== "number") {
 | 
			
		||||
    errors++
 | 
			
		||||
}
 | 
			
		||||
if (typeof config.phone_validation_countrycode !== "string") {
 | 
			
		||||
    errors++
 | 
			
		||||
}
 | 
			
		||||
if (config.phone_validation_countrycode.length !== 2) {
 | 
			
		||||
    errors++
 | 
			
		||||
}
 | 
			
		||||
if (typeof config.development !== "boolean") {
 | 
			
		||||
    errors++
 | 
			
		||||
}
 | 
			
		||||
if (config.mailer_url == "" || config.mailer_key == "") {
 | 
			
		||||
    errors++;
 | 
			
		||||
}
 | 
			
		||||
function getPhoneCodeLocale(): CountryCode {
 | 
			
		||||
    return (process.env.PHONE_COUNTRYCODE as CountryCode);
 | 
			
		||||
}
 | 
			
		||||
function getPostalCodeLocale(): any {
 | 
			
		||||
    try {
 | 
			
		||||
        const stringArray: String[] = ValidatorJS.isPostalCodeLocales;
 | 
			
		||||
@@ -31,4 +40,11 @@ function getPostalCodeLocale(): any {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
function getDataSeeding(): Boolean {
 | 
			
		||||
    try {
 | 
			
		||||
        return JSON.parse(process.env.SEED_TEST_DATA);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
export let e = errors
 | 
			
		||||
@@ -1,104 +1,106 @@
 | 
			
		||||
import { Body, CookieParam, JsonController, Param, Post, Req, Res } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { IllegalJWTError, InvalidCredentialsError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UsernameOrEmailNeededError } from '../errors/AuthError';
 | 
			
		||||
import { UserNotFoundError } from '../errors/UserErrors';
 | 
			
		||||
import { CreateAuth } from '../models/actions/CreateAuth';
 | 
			
		||||
import { CreateResetToken } from '../models/actions/CreateResetToken';
 | 
			
		||||
import { HandleLogout } from '../models/actions/HandleLogout';
 | 
			
		||||
import { RefreshAuth } from '../models/actions/RefreshAuth';
 | 
			
		||||
import { ResetPassword } from '../models/actions/ResetPassword';
 | 
			
		||||
import { Auth } from '../models/responses/ResponseAuth';
 | 
			
		||||
import { Logout } from '../models/responses/ResponseLogout';
 | 
			
		||||
 | 
			
		||||
@JsonController('/auth')
 | 
			
		||||
export class AuthController {
 | 
			
		||||
	constructor() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post("/login")
 | 
			
		||||
	@ResponseSchema(Auth)
 | 
			
		||||
	@ResponseSchema(InvalidCredentialsError)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError)
 | 
			
		||||
	@ResponseSchema(UsernameOrEmailNeededError)
 | 
			
		||||
	@ResponseSchema(PasswordNeededError)
 | 
			
		||||
	@ResponseSchema(InvalidCredentialsError)
 | 
			
		||||
	@OpenAPI({ description: 'Login with your username/email and password. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)' })
 | 
			
		||||
	async login(@Body({ validate: true }) createAuth: CreateAuth, @Res() response: any) {
 | 
			
		||||
		let auth;
 | 
			
		||||
		try {
 | 
			
		||||
			auth = await createAuth.toAuth();
 | 
			
		||||
			response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
 | 
			
		||||
			response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
 | 
			
		||||
			return response.send(auth)
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post("/logout")
 | 
			
		||||
	@ResponseSchema(Logout)
 | 
			
		||||
	@ResponseSchema(InvalidCredentialsError)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError)
 | 
			
		||||
	@ResponseSchema(UsernameOrEmailNeededError)
 | 
			
		||||
	@ResponseSchema(PasswordNeededError)
 | 
			
		||||
	@ResponseSchema(InvalidCredentialsError)
 | 
			
		||||
	@OpenAPI({ description: 'Logout using your refresh token. <br> This instantly invalidates all your access and refresh tokens.', security: [{ "RefreshTokenCookie": [] }] })
 | 
			
		||||
	async logout(@Body({ validate: true }) handleLogout: HandleLogout, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any) {
 | 
			
		||||
		if (refresh_token && refresh_token.length != 0 && handleLogout.token == undefined) {
 | 
			
		||||
			handleLogout.token = refresh_token;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let logout;
 | 
			
		||||
		try {
 | 
			
		||||
			logout = await handleLogout.logout()
 | 
			
		||||
			await response.cookie('lfk_backend__refresh_token', "expired", { expires: new Date(Date.now()), httpOnly: true });
 | 
			
		||||
			response.cookie('lfk_backend__refresh_token_expires_at', "expired", { expires: new Date(Date.now()), httpOnly: true });
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
		return response.send(logout)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post("/refresh")
 | 
			
		||||
	@ResponseSchema(Auth)
 | 
			
		||||
	@ResponseSchema(JwtNotProvidedError)
 | 
			
		||||
	@ResponseSchema(IllegalJWTError)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError)
 | 
			
		||||
	@ResponseSchema(RefreshTokenCountInvalidError)
 | 
			
		||||
	@OpenAPI({ description: 'Refresh your access and refresh tokens using a valid refresh token. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)', security: [{ "RefreshTokenCookie": [] }] })
 | 
			
		||||
	async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any, @Req() req: any) {
 | 
			
		||||
		if (refresh_token && refresh_token.length != 0 && refreshAuth.token == undefined) {
 | 
			
		||||
			refreshAuth.token = refresh_token;
 | 
			
		||||
		}
 | 
			
		||||
		console.log(req.headers)
 | 
			
		||||
		let auth;
 | 
			
		||||
		try {
 | 
			
		||||
			auth = await refreshAuth.toAuth();
 | 
			
		||||
			response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
 | 
			
		||||
			response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
		return response.send(auth)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post("/reset")
 | 
			
		||||
	@ResponseSchema(Auth)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError)
 | 
			
		||||
	@ResponseSchema(UsernameOrEmailNeededError)
 | 
			
		||||
	@OpenAPI({ description: "Request a password reset token. <br> This will provide you with a reset token that you can use by posting to /api/auth/reset/{token}." })
 | 
			
		||||
	async getResetToken(@Body({ validate: true }) passwordReset: CreateResetToken) {
 | 
			
		||||
		//This really shouldn't just get returned, but sent via mail or sth like that. But for dev only this is fine.
 | 
			
		||||
		return { "resetToken": await passwordReset.toResetToken() };
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post("/reset/:token")
 | 
			
		||||
	@ResponseSchema(Auth)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError)
 | 
			
		||||
	@ResponseSchema(UsernameOrEmailNeededError)
 | 
			
		||||
	@OpenAPI({ description: "Reset a user's utilising a valid password reset token. <br> This will set the user's password to the one you provided in the body. <br> To get a reset token post to /api/auth/reset with your username." })
 | 
			
		||||
	async resetPassword(@Param("token") token: string, @Body({ validate: true }) passwordReset: ResetPassword) {
 | 
			
		||||
		passwordReset.resetToken = token;
 | 
			
		||||
		return await passwordReset.resetPassword();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
import { Body, CookieParam, JsonController, Param, Post, QueryParam, Req, Res } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { IllegalJWTError, InvalidCredentialsError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UsernameOrEmailNeededError } from '../errors/AuthError';
 | 
			
		||||
import { MailSendingError } from '../errors/MailErrors';
 | 
			
		||||
import { UserNotFoundError } from '../errors/UserErrors';
 | 
			
		||||
import { Mailer } from '../mailer';
 | 
			
		||||
import { CreateAuth } from '../models/actions/create/CreateAuth';
 | 
			
		||||
import { CreateResetToken } from '../models/actions/create/CreateResetToken';
 | 
			
		||||
import { HandleLogout } from '../models/actions/HandleLogout';
 | 
			
		||||
import { RefreshAuth } from '../models/actions/RefreshAuth';
 | 
			
		||||
import { ResetPassword } from '../models/actions/ResetPassword';
 | 
			
		||||
import { ResponseAuth } from '../models/responses/ResponseAuth';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { Logout } from '../models/responses/ResponseLogout';
 | 
			
		||||
 | 
			
		||||
@JsonController('/auth')
 | 
			
		||||
export class AuthController {
 | 
			
		||||
 | 
			
		||||
	@Post("/login")
 | 
			
		||||
	@ResponseSchema(ResponseAuth)
 | 
			
		||||
	@ResponseSchema(InvalidCredentialsError)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError)
 | 
			
		||||
	@ResponseSchema(UsernameOrEmailNeededError)
 | 
			
		||||
	@ResponseSchema(PasswordNeededError)
 | 
			
		||||
	@ResponseSchema(InvalidCredentialsError)
 | 
			
		||||
	@OpenAPI({ description: 'Login with your username/email and password. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)' })
 | 
			
		||||
	async login(@Body({ validate: true }) createAuth: CreateAuth, @Res() response: any) {
 | 
			
		||||
		let auth;
 | 
			
		||||
		try {
 | 
			
		||||
			auth = await createAuth.toAuth();
 | 
			
		||||
			response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
 | 
			
		||||
			response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
 | 
			
		||||
			return response.send(auth)
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post("/logout")
 | 
			
		||||
	@ResponseSchema(Logout)
 | 
			
		||||
	@ResponseSchema(InvalidCredentialsError)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError)
 | 
			
		||||
	@ResponseSchema(UsernameOrEmailNeededError)
 | 
			
		||||
	@ResponseSchema(PasswordNeededError)
 | 
			
		||||
	@ResponseSchema(InvalidCredentialsError)
 | 
			
		||||
	@OpenAPI({ description: 'Logout using your refresh token. <br> This instantly invalidates all your access and refresh tokens.', security: [{ "RefreshTokenCookie": [] }] })
 | 
			
		||||
	async logout(@Body({ validate: true }) handleLogout: HandleLogout, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any) {
 | 
			
		||||
		if (refresh_token && refresh_token.length != 0 && handleLogout.token == undefined) {
 | 
			
		||||
			handleLogout.token = refresh_token;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let logout;
 | 
			
		||||
		try {
 | 
			
		||||
			logout = await handleLogout.logout()
 | 
			
		||||
			await response.cookie('lfk_backend__refresh_token', "expired", { expires: new Date(Date.now()), httpOnly: true });
 | 
			
		||||
			response.cookie('lfk_backend__refresh_token_expires_at', "expired", { expires: new Date(Date.now()), httpOnly: true });
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
		return response.send(logout)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post("/refresh")
 | 
			
		||||
	@ResponseSchema(ResponseAuth)
 | 
			
		||||
	@ResponseSchema(JwtNotProvidedError)
 | 
			
		||||
	@ResponseSchema(IllegalJWTError)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError)
 | 
			
		||||
	@ResponseSchema(RefreshTokenCountInvalidError)
 | 
			
		||||
	@OpenAPI({ description: 'Refresh your access and refresh tokens using a valid refresh token. <br> You will receive: \n * access token (use it as a bearer token) \n * refresh token (will also be sent as a cookie)', security: [{ "RefreshTokenCookie": [] }] })
 | 
			
		||||
	async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any, @Req() req: any) {
 | 
			
		||||
		if (refresh_token && refresh_token.length != 0 && refreshAuth.token == undefined) {
 | 
			
		||||
			refreshAuth.token = refresh_token;
 | 
			
		||||
		}
 | 
			
		||||
		let auth;
 | 
			
		||||
		try {
 | 
			
		||||
			auth = await refreshAuth.toAuth();
 | 
			
		||||
			response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
 | 
			
		||||
			response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
		return response.send(auth)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post("/reset")
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 200 })
 | 
			
		||||
	@ResponseSchema(UserNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(UsernameOrEmailNeededError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(MailSendingError, { statusCode: 500 })
 | 
			
		||||
	@OpenAPI({ description: "Request a password reset token. <br> This will provide you with a reset token that you can use by posting to /api/auth/reset/{token}." })
 | 
			
		||||
	async getResetToken(@Body({ validate: true }) passwordReset: CreateResetToken, @QueryParam("locale") locale: string = "en") {
 | 
			
		||||
		const reset_token: string = await passwordReset.toResetToken();
 | 
			
		||||
		await Mailer.sendResetMail(passwordReset.email, reset_token, locale);
 | 
			
		||||
		return new ResponseEmpty();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post("/reset/:token")
 | 
			
		||||
	@ResponseSchema(ResponseAuth)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError)
 | 
			
		||||
	@ResponseSchema(UsernameOrEmailNeededError)
 | 
			
		||||
	@OpenAPI({ description: "Reset a user's utilising a valid password reset token. <br> This will set the user's password to the one you provided in the body. <br> To get a reset token post to /api/auth/reset with your username." })
 | 
			
		||||
	async resetPassword(@Param("token") token: string, @Body({ validate: true }) passwordReset: ResetPassword) {
 | 
			
		||||
		passwordReset.resetToken = token;
 | 
			
		||||
		return await passwordReset.resetPassword();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										145
									
								
								src/controllers/DonationController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/controllers/DonationController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,145 @@
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { DonationIdsNotMatchingError, DonationNotFoundError } from '../errors/DonationErrors';
 | 
			
		||||
import { DonorNotFoundError } from '../errors/DonorErrors';
 | 
			
		||||
import { RunnerNotFoundError } from '../errors/RunnerErrors';
 | 
			
		||||
import { CreateDistanceDonation } from '../models/actions/create/CreateDistanceDonation';
 | 
			
		||||
import { CreateFixedDonation } from '../models/actions/create/CreateFixedDonation';
 | 
			
		||||
import { UpdateDistanceDonation } from '../models/actions/update/UpdateDistanceDonation';
 | 
			
		||||
import { UpdateFixedDonation } from '../models/actions/update/UpdateFixedDonation';
 | 
			
		||||
import { DistanceDonation } from '../models/entities/DistanceDonation';
 | 
			
		||||
import { Donation } from '../models/entities/Donation';
 | 
			
		||||
import { FixedDonation } from '../models/entities/FixedDonation';
 | 
			
		||||
import { ResponseDistanceDonation } from '../models/responses/ResponseDistanceDonation';
 | 
			
		||||
import { ResponseDonation } from '../models/responses/ResponseDonation';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
 | 
			
		||||
@JsonController('/donations')
 | 
			
		||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
export class DonationController {
 | 
			
		||||
	private donationRepository: Repository<Donation>;
 | 
			
		||||
	private distanceDonationRepository: Repository<DistanceDonation>;
 | 
			
		||||
	private fixedDonationRepository: Repository<FixedDonation>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Gets the repository of this controller's model/entity.
 | 
			
		||||
	 */
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.donationRepository = getConnectionManager().get().getRepository(Donation);
 | 
			
		||||
		this.distanceDonationRepository = getConnectionManager().get().getRepository(DistanceDonation);
 | 
			
		||||
		this.fixedDonationRepository = getConnectionManager().get().getRepository(FixedDonation);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get()
 | 
			
		||||
	@Authorized("DONATION:GET")
 | 
			
		||||
	@ResponseSchema(ResponseDonation, { isArray: true })
 | 
			
		||||
	@ResponseSchema(ResponseDistanceDonation, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all donations (fixed or distance based) from all donors. <br> This includes the donations\'s runner\'s distance ran(if distance donation).' })
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseDonations: ResponseDonation[] = new Array<ResponseDonation>();
 | 
			
		||||
		const donations = await this.donationRepository.find({ relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] });
 | 
			
		||||
		donations.forEach(donation => {
 | 
			
		||||
			responseDonations.push(donation.toResponse());
 | 
			
		||||
		});
 | 
			
		||||
		return responseDonations;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id')
 | 
			
		||||
	@Authorized("DONATION:GET")
 | 
			
		||||
	@ResponseSchema(ResponseDonation)
 | 
			
		||||
	@ResponseSchema(ResponseDistanceDonation)
 | 
			
		||||
	@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(DonationNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about the donation whose id got provided. This includes the donation\'s runner\'s distance ran (if distance donation).' })
 | 
			
		||||
	async getOne(@Param('id') id: number) {
 | 
			
		||||
		let donation = await this.donationRepository.findOne({ id: id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })
 | 
			
		||||
		if (!donation) { throw new DonationNotFoundError(); }
 | 
			
		||||
		return donation.toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post('/fixed')
 | 
			
		||||
	@Authorized("DONATION:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseDonation)
 | 
			
		||||
	@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: 'Create a fixed donation (not distance donation - use /donations/distance instead). <br> Please rmemember to provide the donation\'s donors\'s id and amount.' })
 | 
			
		||||
	async postFixed(@Body({ validate: true }) createDonation: CreateFixedDonation) {
 | 
			
		||||
		let donation = await createDonation.toEntity();
 | 
			
		||||
		donation = await this.fixedDonationRepository.save(donation);
 | 
			
		||||
		return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post('/distance')
 | 
			
		||||
	@Authorized("DONATION:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseDistanceDonation)
 | 
			
		||||
	@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: 'Create a distance donation (not fixed donation - use /donations/fixed instead). <br> Please rmemember to provide the donation\'s donors\'s and runner\s ids and amount per distance (kilometer).' })
 | 
			
		||||
	async postDistance(@Body({ validate: true }) createDonation: CreateDistanceDonation) {
 | 
			
		||||
		let donation = await createDonation.toEntity();
 | 
			
		||||
		donation = await this.distanceDonationRepository.save(donation);
 | 
			
		||||
		return (await this.distanceDonationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/fixed/:id')
 | 
			
		||||
	@Authorized("DONATION:UPDATE")
 | 
			
		||||
	@ResponseSchema(ResponseDonation)
 | 
			
		||||
	@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(DonationIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Update the fixed donation (not distance donation - use /donations/distance instead) whose id you provided. <br> Please remember that ids can't be changed and amounts must be positive." })
 | 
			
		||||
	async putFixed(@Param('id') id: number, @Body({ validate: true }) donation: UpdateFixedDonation) {
 | 
			
		||||
		let oldDonation = await this.fixedDonationRepository.findOne({ id: id });
 | 
			
		||||
 | 
			
		||||
		if (!oldDonation) {
 | 
			
		||||
			throw new DonationNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldDonation.id != donation.id) {
 | 
			
		||||
			throw new DonationIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.fixedDonationRepository.save(await donation.update(oldDonation));
 | 
			
		||||
		return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/distance/:id')
 | 
			
		||||
	@Authorized("DONATION:UPDATE")
 | 
			
		||||
	@ResponseSchema(ResponseDonation)
 | 
			
		||||
	@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(DonationIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Update the distance donation (not fixed donation - use /donations/fixed instead) whose id you provided. <br> Please remember that ids can't be changed and amountPerDistance must be positive." })
 | 
			
		||||
	async putDistance(@Param('id') id: number, @Body({ validate: true }) donation: UpdateDistanceDonation) {
 | 
			
		||||
		let oldDonation = await this.distanceDonationRepository.findOne({ id: id });
 | 
			
		||||
 | 
			
		||||
		if (!oldDonation) {
 | 
			
		||||
			throw new DonationNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldDonation.id != donation.id) {
 | 
			
		||||
			throw new DonationIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.distanceDonationRepository.save(await donation.update(oldDonation));
 | 
			
		||||
		return (await this.distanceDonationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
	@Authorized("DONATION:DELETE")
 | 
			
		||||
	@ResponseSchema(ResponseDonation)
 | 
			
		||||
	@ResponseSchema(ResponseDistanceDonation)
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: 'Delete the donation whose id you provided. <br> If no donation with this id exists it will just return 204(no content).' })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let donation = await this.donationRepository.findOne({ id: id });
 | 
			
		||||
		if (!donation) { return null; }
 | 
			
		||||
		const responseScan = await this.donationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] });
 | 
			
		||||
 | 
			
		||||
		await this.donationRepository.delete(donation);
 | 
			
		||||
		return responseScan.toResponse();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										113
									
								
								src/controllers/DonorController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/controllers/DonorController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { DonorHasDonationsError, DonorIdsNotMatchingError, DonorNotFoundError } from '../errors/DonorErrors';
 | 
			
		||||
import { CreateDonor } from '../models/actions/create/CreateDonor';
 | 
			
		||||
import { UpdateDonor } from '../models/actions/update/UpdateDonor';
 | 
			
		||||
import { Donor } from '../models/entities/Donor';
 | 
			
		||||
import { ResponseDonor } from '../models/responses/ResponseDonor';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { DonationController } from './DonationController';
 | 
			
		||||
 | 
			
		||||
@JsonController('/donors')
 | 
			
		||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
export class DonorController {
 | 
			
		||||
	private donorRepository: Repository<Donor>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Gets the repository of this controller's model/entity.
 | 
			
		||||
	 */
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.donorRepository = getConnectionManager().get().getRepository(Donor);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get()
 | 
			
		||||
	@Authorized("DONOR:GET")
 | 
			
		||||
	@ResponseSchema(ResponseDonor, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all donor. <br> This includes the donor\'s current donation amount.' })
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseDonors: ResponseDonor[] = new Array<ResponseDonor>();
 | 
			
		||||
		const donors = await this.donorRepository.find({ relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] });
 | 
			
		||||
		donors.forEach(donor => {
 | 
			
		||||
			responseDonors.push(new ResponseDonor(donor));
 | 
			
		||||
		});
 | 
			
		||||
		return responseDonors;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id')
 | 
			
		||||
	@Authorized("DONOR:GET")
 | 
			
		||||
	@ResponseSchema(ResponseDonor)
 | 
			
		||||
	@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(DonorNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about the donor whose id got provided. <br> This includes the donor\'s current donation amount.' })
 | 
			
		||||
	async getOne(@Param('id') id: number) {
 | 
			
		||||
		let donor = await this.donorRepository.findOne({ id: id }, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] })
 | 
			
		||||
		if (!donor) { throw new DonorNotFoundError(); }
 | 
			
		||||
		return new ResponseDonor(donor);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post()
 | 
			
		||||
	@Authorized("DONOR:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseDonor)
 | 
			
		||||
	@OpenAPI({ description: 'Create a new donor.' })
 | 
			
		||||
	async post(@Body({ validate: true }) createRunner: CreateDonor) {
 | 
			
		||||
		let donor;
 | 
			
		||||
		try {
 | 
			
		||||
			donor = await createRunner.toEntity();
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		donor = await this.donorRepository.save(donor)
 | 
			
		||||
		return new ResponseDonor(await this.donorRepository.findOne(donor, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/:id')
 | 
			
		||||
	@Authorized("DONOR:UPDATE")
 | 
			
		||||
	@ResponseSchema(ResponseDonor)
 | 
			
		||||
	@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(DonorIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Update the donor whose id you provided. <br> Please remember that ids can't be changed." })
 | 
			
		||||
	async put(@Param('id') id: number, @Body({ validate: true }) donor: UpdateDonor) {
 | 
			
		||||
		let oldDonor = await this.donorRepository.findOne({ id: id });
 | 
			
		||||
 | 
			
		||||
		if (!oldDonor) {
 | 
			
		||||
			throw new DonorNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldDonor.id != donor.id) {
 | 
			
		||||
			throw new DonorIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.donorRepository.save(await donor.update(oldDonor));
 | 
			
		||||
		return new ResponseDonor(await this.donorRepository.findOne({ id: id }, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
	@Authorized("DONOR:DELETE")
 | 
			
		||||
	@ResponseSchema(ResponseDonor)
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: 'Delete the donor whose id you provided. <br> If no donor with this id exists it will just return 204(no content). <br> If the donor still has donations associated this will fail, please provide the query param ?force=true to delete the donor with all associated donations.' })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let donor = await this.donorRepository.findOne({ id: id });
 | 
			
		||||
		if (!donor) { return null; }
 | 
			
		||||
		const responseDonor = await this.donorRepository.findOne(donor, { relations: ['donations', 'donations.runner', 'donations.runner.scans', 'donations.runner.scans.track'] });
 | 
			
		||||
 | 
			
		||||
		if (!donor) {
 | 
			
		||||
			throw new DonorNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const donorDonations = (await this.donorRepository.findOne({ id: donor.id }, { relations: ["donations"] })).donations;
 | 
			
		||||
		if (donorDonations.length > 0 && !force) {
 | 
			
		||||
			throw new DonorHasDonationsError();
 | 
			
		||||
		}
 | 
			
		||||
		const donationController = new DonationController();
 | 
			
		||||
		for (let donation of donorDonations) {
 | 
			
		||||
			await donationController.remove(donation.id, force);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.donorRepository.delete(donor);
 | 
			
		||||
		return new ResponseDonor(responseDonor);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										107
									
								
								src/controllers/GroupContactController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/controllers/GroupContactController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnection, getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { GroupContactIdsNotMatchingError, GroupContactNotFoundError } from '../errors/GroupContactErrors';
 | 
			
		||||
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
 | 
			
		||||
import { CreateGroupContact } from '../models/actions/create/CreateGroupContact';
 | 
			
		||||
import { UpdateGroupContact } from '../models/actions/update/UpdateGroupContact';
 | 
			
		||||
import { GroupContact } from '../models/entities/GroupContact';
 | 
			
		||||
import { RunnerGroup } from '../models/entities/RunnerGroup';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseGroupContact } from '../models/responses/ResponseGroupContact';
 | 
			
		||||
 | 
			
		||||
@JsonController('/contacts')
 | 
			
		||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
export class GroupContactController {
 | 
			
		||||
	private contactRepository: Repository<GroupContact>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Gets the repository of this controller's model/entity.
 | 
			
		||||
	 */
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.contactRepository = getConnectionManager().get().getRepository(GroupContact);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get()
 | 
			
		||||
	@Authorized("CONTACT:GET")
 | 
			
		||||
	@ResponseSchema(ResponseGroupContact, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all contacts. <br> This includes the contact\'s associated groups.' })
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseContacts: ResponseGroupContact[] = new Array<ResponseGroupContact>();
 | 
			
		||||
		const contacts = await this.contactRepository.find({ relations: ['groups', 'groups.parentGroup'] });
 | 
			
		||||
		contacts.forEach(contact => {
 | 
			
		||||
			responseContacts.push(contact.toResponse());
 | 
			
		||||
		});
 | 
			
		||||
		return responseContacts;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id')
 | 
			
		||||
	@Authorized("CONTACT:GET")
 | 
			
		||||
	@ResponseSchema(ResponseGroupContact)
 | 
			
		||||
	@ResponseSchema(GroupContactNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(GroupContactNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about the contact whose id got provided. <br> This includes the contact\'s associated groups.' })
 | 
			
		||||
	async getOne(@Param('id') id: number) {
 | 
			
		||||
		let contact = await this.contactRepository.findOne({ id: id }, { relations: ['groups', 'groups.parentGroup'] })
 | 
			
		||||
		if (!contact) { throw new GroupContactNotFoundError(); }
 | 
			
		||||
		return contact.toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post()
 | 
			
		||||
	@Authorized("CONTACT:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseGroupContact)
 | 
			
		||||
	@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: 'Create a new contact.' })
 | 
			
		||||
	async post(@Body({ validate: true }) createContact: CreateGroupContact) {
 | 
			
		||||
		let contact;
 | 
			
		||||
		try {
 | 
			
		||||
			contact = await createContact.toEntity();
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		contact = await this.contactRepository.save(contact)
 | 
			
		||||
		return (await this.contactRepository.findOne({ id: contact.id }, { relations: ['groups', 'groups.parentGroup'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/:id')
 | 
			
		||||
	@Authorized("CONTACT:UPDATE")
 | 
			
		||||
	@ResponseSchema(ResponseGroupContact)
 | 
			
		||||
	@ResponseSchema(GroupContactNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(GroupContactIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: "Update the contact whose id you provided. <br> Please remember that ids can't be changed." })
 | 
			
		||||
	async put(@Param('id') id: number, @Body({ validate: true }) contact: UpdateGroupContact) {
 | 
			
		||||
		let oldContact = await this.contactRepository.findOne({ id: id });
 | 
			
		||||
 | 
			
		||||
		if (!oldContact) {
 | 
			
		||||
			throw new GroupContactNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldContact.id != contact.id) {
 | 
			
		||||
			throw new GroupContactIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.contactRepository.save(await contact.update(oldContact));
 | 
			
		||||
		return (await this.contactRepository.findOne({ id: contact.id }, { relations: ['groups', 'groups.parentGroup'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
	@Authorized("CONTACT:DELETE")
 | 
			
		||||
	@ResponseSchema(ResponseGroupContact)
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: 'Delete the contact whose id you provided. <br> If no contact with this id exists it will just return 204(no content). <br> This won\'t delete any groups associated with the contact.' })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let contact = await this.contactRepository.findOne({ id: id });
 | 
			
		||||
		if (!contact) { return null; }
 | 
			
		||||
		const responseContact = await this.contactRepository.findOne(contact, { relations: ['groups', 'groups.parentGroup'] });
 | 
			
		||||
		for (let group of responseContact.groups) {
 | 
			
		||||
			group.contact = null;
 | 
			
		||||
			await getConnection().getRepository(RunnerGroup).save(group);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.contactRepository.delete(contact);
 | 
			
		||||
		return responseContact.toResponse();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -36,7 +36,7 @@ export class ImportController {
 | 
			
		||||
        return responseRunners;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Post('/organisations/:id/import')
 | 
			
		||||
    @Post('/organizations/:id/import')
 | 
			
		||||
    @ContentType("application/json")
 | 
			
		||||
    @ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
 | 
			
		||||
    @ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
 | 
			
		||||
@@ -78,7 +78,7 @@ export class ImportController {
 | 
			
		||||
        return await this.postJSON(importRunners, groupID);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Post('/organisations/:id/import/csv')
 | 
			
		||||
    @Post('/organizations/:id/import/csv')
 | 
			
		||||
    @ContentType("application/json")
 | 
			
		||||
    @UseBefore(RawBodyMiddleware)
 | 
			
		||||
    @ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										90
									
								
								src/controllers/MeController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/controllers/MeController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
import { Body, CurrentUser, Delete, Get, JsonController, OnUndefined, Put, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserDeletionNotConfirmedError, UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors';
 | 
			
		||||
import { UpdateUser } from '../models/actions/update/UpdateUser';
 | 
			
		||||
import { User } from '../models/entities/User';
 | 
			
		||||
import { ResponseUser } from '../models/responses/ResponseUser';
 | 
			
		||||
import { ResponseUserPermissions } from '../models/responses/ResponseUserPermissions';
 | 
			
		||||
import { PermissionController } from './PermissionController';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@JsonController('/users/me')
 | 
			
		||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
export class MeController {
 | 
			
		||||
	private userRepository: Repository<User>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Gets the repository of this controller's model/entity.
 | 
			
		||||
	 */
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.userRepository = getConnectionManager().get().getRepository(User);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/')
 | 
			
		||||
	@ResponseSchema(ResponseUser)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(UserNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about yourself.' })
 | 
			
		||||
	async get(@CurrentUser() currentUser: User) {
 | 
			
		||||
		let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
 | 
			
		||||
		if (!user) { throw new UserNotFoundError(); }
 | 
			
		||||
		return new ResponseUser(user);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/permissions')
 | 
			
		||||
	@ResponseSchema(ResponseUserPermissions)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(UserNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all permissions granted to the you sorted into directly granted and inherited as permission response objects.' })
 | 
			
		||||
	async getPermissions(@CurrentUser() currentUser: User) {
 | 
			
		||||
		let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
 | 
			
		||||
		if (!user) { throw new UserNotFoundError(); }
 | 
			
		||||
		return new ResponseUserPermissions(user);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/')
 | 
			
		||||
	@ResponseSchema(ResponseUser)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Update the yourself. <br> You can't edit your own permissions or group memberships here - Please use the /api/users/:id enpoint instead. <br> Please remember that ids can't be changed." })
 | 
			
		||||
	async put(@CurrentUser() currentUser: User, @Body({ validate: true }) updateUser: UpdateUser) {
 | 
			
		||||
		let oldUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['groups'] });
 | 
			
		||||
		updateUser.groups = oldUser.groups.map(g => g.id);
 | 
			
		||||
 | 
			
		||||
		if (!oldUser) {
 | 
			
		||||
			throw new UserNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldUser.id != updateUser.id) {
 | 
			
		||||
			throw new UserIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
		await this.userRepository.save(await updateUser.update(oldUser));
 | 
			
		||||
 | 
			
		||||
		return new ResponseUser(await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/')
 | 
			
		||||
	@ResponseSchema(ResponseUser)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: 'Delete yourself. <br> You have to confirm your decision by providing the ?force=true query param. <br> If there are any permissions directly granted to you they will get deleted as well.' })
 | 
			
		||||
	async remove(@CurrentUser() currentUser: User, @QueryParam("force") force: boolean) {
 | 
			
		||||
		if (!force) { throw new UserDeletionNotConfirmedError; }
 | 
			
		||||
		if (!currentUser) { return UserNotFoundError; }
 | 
			
		||||
		const responseUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] });;
 | 
			
		||||
 | 
			
		||||
		const permissionControler = new PermissionController();
 | 
			
		||||
		for (let permission of responseUser.permissions) {
 | 
			
		||||
			await permissionControler.remove(permission.id, true);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.userRepository.delete(currentUser);
 | 
			
		||||
		return new ResponseUser(responseUser);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -3,8 +3,8 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { PermissionIdsNotMatchingError, PermissionNeedsPrincipalError, PermissionNotFoundError } from '../errors/PermissionErrors';
 | 
			
		||||
import { PrincipalNotFoundError } from '../errors/PrincipalErrors';
 | 
			
		||||
import { CreatePermission } from '../models/actions/CreatePermission';
 | 
			
		||||
import { UpdatePermission } from '../models/actions/UpdatePermission';
 | 
			
		||||
import { CreatePermission } from '../models/actions/create/CreatePermission';
 | 
			
		||||
import { UpdatePermission } from '../models/actions/update/UpdatePermission';
 | 
			
		||||
import { Permission } from '../models/entities/Permission';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponsePermission } from '../models/responses/ResponsePermission';
 | 
			
		||||
@@ -58,7 +58,7 @@ export class PermissionController {
 | 
			
		||||
    async post(@Body({ validate: true }) createPermission: CreatePermission) {
 | 
			
		||||
        let permission;
 | 
			
		||||
        try {
 | 
			
		||||
            permission = await createPermission.toPermission();
 | 
			
		||||
            permission = await createPermission.toEntity();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            throw error;
 | 
			
		||||
        }
 | 
			
		||||
@@ -90,13 +90,13 @@ export class PermissionController {
 | 
			
		||||
        if (oldPermission.id != permission.id) {
 | 
			
		||||
            throw new PermissionIdsNotMatchingError();
 | 
			
		||||
        }
 | 
			
		||||
        let existingPermission = await this.permissionRepository.findOne({ target: permission.target, action: permission.action, principal: permission.principal }, { relations: ['principal'] });
 | 
			
		||||
        let existingPermission = await this.permissionRepository.findOne({ target: permission.target, action: permission.action, principal: await permission.getPrincipal() }, { relations: ['principal'] });
 | 
			
		||||
        if (existingPermission) {
 | 
			
		||||
            await this.remove(permission.id, true);
 | 
			
		||||
            return new ResponsePermission(existingPermission);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.permissionRepository.save(await permission.updatePermission(oldPermission));
 | 
			
		||||
        await this.permissionRepository.save(await permission.update(oldPermission));
 | 
			
		||||
 | 
			
		||||
        return new ResponsePermission(await this.permissionRepository.findOne({ id: permission.id }, { relations: ['principal'] }));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										131
									
								
								src/controllers/RunnerCardController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								src/controllers/RunnerCardController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,131 @@
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { RunnerCardHasScansError, RunnerCardIdsNotMatchingError, RunnerCardNotFoundError } from '../errors/RunnerCardErrors';
 | 
			
		||||
import { RunnerNotFoundError } from '../errors/RunnerErrors';
 | 
			
		||||
import { CreateRunnerCard } from '../models/actions/create/CreateRunnerCard';
 | 
			
		||||
import { UpdateRunnerCard } from '../models/actions/update/UpdateRunnerCard';
 | 
			
		||||
import { RunnerCard } from '../models/entities/RunnerCard';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseRunnerCard } from '../models/responses/ResponseRunnerCard';
 | 
			
		||||
import { ScanController } from './ScanController';
 | 
			
		||||
 | 
			
		||||
@JsonController('/cards')
 | 
			
		||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
export class RunnerCardController {
 | 
			
		||||
	private cardRepository: Repository<RunnerCard>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Gets the repository of this controller's model/entity.
 | 
			
		||||
	 */
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.cardRepository = getConnectionManager().get().getRepository(RunnerCard);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get()
 | 
			
		||||
	@Authorized("CARD:GET")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerCard, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all card.' })
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>();
 | 
			
		||||
		const cards = await this.cardRepository.find({ relations: ['runner', 'runner.group', 'runner.group.parentGroup'] });
 | 
			
		||||
		cards.forEach(card => {
 | 
			
		||||
			responseCards.push(new ResponseRunnerCard(card));
 | 
			
		||||
		});
 | 
			
		||||
		return responseCards;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id')
 | 
			
		||||
	@Authorized("CARD:GET")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerCard)
 | 
			
		||||
	@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(RunnerCardNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: "Lists all information about the card whose id got provided." })
 | 
			
		||||
	async getOne(@Param('id') id: number) {
 | 
			
		||||
		let card = await this.cardRepository.findOne({ id: id }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] });
 | 
			
		||||
		if (!card) { throw new RunnerCardNotFoundError(); }
 | 
			
		||||
		return card.toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post('/bulk')
 | 
			
		||||
	@Authorized("CARD:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 200 })
 | 
			
		||||
	@OpenAPI({ description: "Create blank cards in bulk. <br> Just provide the count as a query param and wait for the 200 response. <br> You can provide the 'returnCards' query param if you want to receive the RESPONSERUNNERCARD objects in the response." })
 | 
			
		||||
	async postBlancoBulk(@QueryParam("count") count: number, @QueryParam("returnCards") returnCards: boolean = false) {
 | 
			
		||||
		let createPromises = new Array<any>();
 | 
			
		||||
		for (let index = 0; index < count; index++) {
 | 
			
		||||
			createPromises.push(this.cardRepository.save({ runner: null, enabled: true }))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const cards = await Promise.all(createPromises);
 | 
			
		||||
 | 
			
		||||
		if (returnCards) {
 | 
			
		||||
			let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>();
 | 
			
		||||
			for await (let card of cards) {
 | 
			
		||||
				let dbCard = await this.cardRepository.findOne({ id: card.id });
 | 
			
		||||
				responseCards.push(new ResponseRunnerCard(dbCard));
 | 
			
		||||
			}
 | 
			
		||||
			return responseCards;
 | 
			
		||||
		}
 | 
			
		||||
		let response = new ResponseEmpty();
 | 
			
		||||
		response.response = `Created ${count} new blanco cards.`
 | 
			
		||||
		return response;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post()
 | 
			
		||||
	@Authorized("CARD:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerCard)
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: "Create a new card. <br> You can provide a associated runner by id but you don't have to." })
 | 
			
		||||
	async post(@Body({ validate: true }) createCard: CreateRunnerCard) {
 | 
			
		||||
		let card = await createCard.toEntity();
 | 
			
		||||
		card = await this.cardRepository.save(card);
 | 
			
		||||
		return (await this.cardRepository.findOne({ id: card.id }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/:id')
 | 
			
		||||
	@Authorized("CARD:UPDATE")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerCard)
 | 
			
		||||
	@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(RunnerCardIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Update the card whose id you provided. <br> Scans created via this card will still be associated with the old runner. <br> Please remember that ids can't be changed." })
 | 
			
		||||
	async put(@Param('id') id: number, @Body({ validate: true }) card: UpdateRunnerCard) {
 | 
			
		||||
		let oldCard = await this.cardRepository.findOne({ id: id });
 | 
			
		||||
 | 
			
		||||
		if (!oldCard) {
 | 
			
		||||
			throw new RunnerCardNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldCard.id != card.id) {
 | 
			
		||||
			throw new RunnerCardIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.cardRepository.save(await card.update(oldCard));
 | 
			
		||||
		return (await this.cardRepository.findOne({ id: id }, { relations: ['runner', 'runner.group', 'runner.group.parentGroup'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
	@Authorized("CARD:DELETE")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerCard)
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@ResponseSchema(RunnerCardHasScansError, { statusCode: 406 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: "Delete the card whose id you provided. <br> If no card with this id exists it will just return 204(no content). <br> If the card still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with by this card - please disable it instead or just remove the runner association)." })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let card = await this.cardRepository.findOne({ id: id });
 | 
			
		||||
		if (!card) { return null; }
 | 
			
		||||
 | 
			
		||||
		const cardScans = (await this.cardRepository.findOne({ id: id }, { relations: ["scans"] })).scans;
 | 
			
		||||
		if (cardScans.length != 0 && !force) {
 | 
			
		||||
			throw new RunnerCardHasScansError();
 | 
			
		||||
		}
 | 
			
		||||
		const scanController = new ScanController;
 | 
			
		||||
		for (let scan of cardScans) {
 | 
			
		||||
			await scanController.remove(scan.id, force);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.cardRepository.delete(card);
 | 
			
		||||
		return card.toResponse();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +1,18 @@
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { RunnerGroupNeededError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
 | 
			
		||||
import { RunnerGroupNeededError, RunnerHasDistanceDonationsError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
 | 
			
		||||
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
 | 
			
		||||
import { CreateRunner } from '../models/actions/CreateRunner';
 | 
			
		||||
import { UpdateRunner } from '../models/actions/UpdateRunner';
 | 
			
		||||
import { CreateRunner } from '../models/actions/create/CreateRunner';
 | 
			
		||||
import { UpdateRunner } from '../models/actions/update/UpdateRunner';
 | 
			
		||||
import { Runner } from '../models/entities/Runner';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseRunner } from '../models/responses/ResponseRunner';
 | 
			
		||||
import { ResponseScan } from '../models/responses/ResponseScan';
 | 
			
		||||
import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
 | 
			
		||||
import { DonationController } from './DonationController';
 | 
			
		||||
import { RunnerCardController } from './RunnerCardController';
 | 
			
		||||
import { ScanController } from './ScanController';
 | 
			
		||||
 | 
			
		||||
@JsonController('/runners')
 | 
			
		||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
@@ -27,7 +32,7 @@ export class RunnerController {
 | 
			
		||||
	@OpenAPI({ description: 'Lists all runners from all teams/orgs. <br> This includes the runner\'s group and distance ran.' })
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
 | 
			
		||||
		const runners = await this.runnerRepository.find({ relations: ['scans', 'group'] });
 | 
			
		||||
		const runners = await this.runnerRepository.find({ relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] });
 | 
			
		||||
		runners.forEach(runner => {
 | 
			
		||||
			responseRunners.push(new ResponseRunner(runner));
 | 
			
		||||
		});
 | 
			
		||||
@@ -41,11 +46,36 @@ export class RunnerController {
 | 
			
		||||
	@OnUndefined(RunnerNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about the runner whose id got provided.' })
 | 
			
		||||
	async getOne(@Param('id') id: number) {
 | 
			
		||||
		let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group'] })
 | 
			
		||||
		let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] })
 | 
			
		||||
		if (!runner) { throw new RunnerNotFoundError(); }
 | 
			
		||||
		return new ResponseRunner(runner);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id/scans')
 | 
			
		||||
	@Authorized(["RUNNER:GET", "SCAN:GET"])
 | 
			
		||||
	@ResponseSchema(ResponseScan, { isArray: true })
 | 
			
		||||
	@ResponseSchema(ResponseTrackScan, { isArray: true })
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all scans of the runner whose id got provided. <br> If you only want the valid scans just add the ?onlyValid=true query param.' })
 | 
			
		||||
	async getScans(@Param('id') id: number, onlyValid?: boolean) {
 | 
			
		||||
		let responseScans: ResponseScan[] = new Array<ResponseScan>();
 | 
			
		||||
		let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'scans.track', 'scans.station', 'scans.runner'] })
 | 
			
		||||
		if (!runner) { throw new RunnerNotFoundError(); }
 | 
			
		||||
 | 
			
		||||
		if (!onlyValid) {
 | 
			
		||||
			for (let scan of runner.scans) {
 | 
			
		||||
				responseScans.push(scan.toResponse());
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			for (let scan of runner.validScans) {
 | 
			
		||||
				responseScans.push(scan.toResponse());
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return responseScans;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post()
 | 
			
		||||
	@Authorized("RUNNER:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseRunner)
 | 
			
		||||
@@ -55,13 +85,13 @@ export class RunnerController {
 | 
			
		||||
	async post(@Body({ validate: true }) createRunner: CreateRunner) {
 | 
			
		||||
		let runner;
 | 
			
		||||
		try {
 | 
			
		||||
			runner = await createRunner.toRunner();
 | 
			
		||||
			runner = await createRunner.toEntity();
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		runner = await this.runnerRepository.save(runner)
 | 
			
		||||
		return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group'] }));
 | 
			
		||||
		return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/:id')
 | 
			
		||||
@@ -81,25 +111,47 @@ export class RunnerController {
 | 
			
		||||
			throw new RunnerIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.runnerRepository.save(await runner.updateRunner(oldRunner));
 | 
			
		||||
		return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group'] }));
 | 
			
		||||
		await this.runnerRepository.save(await runner.update(oldRunner));
 | 
			
		||||
		return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
	@Authorized("RUNNER:DELETE")
 | 
			
		||||
	@ResponseSchema(ResponseRunner)
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@ResponseSchema(RunnerHasDistanceDonationsError, { statusCode: 406 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: 'Delete the runner whose id you provided. <br> If no runner with this id exists it will just return 204(no content).' })
 | 
			
		||||
	@OpenAPI({ description: 'Delete the runner whose id you provided. <br> This will also delete all scans and cards associated with the runner. <br> If no runner with this id exists it will just return 204(no content).' })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let runner = await this.runnerRepository.findOne({ id: id });
 | 
			
		||||
		if (!runner) { return null; }
 | 
			
		||||
		const responseRunner = await this.runnerRepository.findOne(runner, { relations: ['scans', 'group'] });
 | 
			
		||||
		const responseRunner = await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards'] });
 | 
			
		||||
 | 
			
		||||
		if (!runner) {
 | 
			
		||||
			throw new RunnerNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const runnerDonations = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["distanceDonations"] })).distanceDonations;
 | 
			
		||||
		if (runnerDonations.length > 0 && !force) {
 | 
			
		||||
			throw new RunnerHasDistanceDonationsError();
 | 
			
		||||
		}
 | 
			
		||||
		const donationController = new DonationController();
 | 
			
		||||
		for (let donation of runnerDonations) {
 | 
			
		||||
			await donationController.remove(donation.id, force);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards;
 | 
			
		||||
		const cardController = new RunnerCardController;
 | 
			
		||||
		for (let card of runnerCards) {
 | 
			
		||||
			await cardController.remove(card.id, force);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans;
 | 
			
		||||
		const scanController = new ScanController;
 | 
			
		||||
		for (let scan of runnerScans) {
 | 
			
		||||
			await scanController.remove(scan.id, force);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.runnerRepository.delete(runner);
 | 
			
		||||
		return new ResponseRunner(responseRunner);
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,127 +0,0 @@
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { RunnerOrganisationHasRunnersError, RunnerOrganisationHasTeamsError, RunnerOrganisationIdsNotMatchingError, RunnerOrganisationNotFoundError } from '../errors/RunnerOrganisationErrors';
 | 
			
		||||
import { CreateRunnerOrganisation } from '../models/actions/CreateRunnerOrganisation';
 | 
			
		||||
import { UpdateRunnerOrganisation } from '../models/actions/UpdateRunnerOrganisation';
 | 
			
		||||
import { RunnerOrganisation } from '../models/entities/RunnerOrganisation';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseRunnerOrganisation } from '../models/responses/ResponseRunnerOrganisation';
 | 
			
		||||
import { RunnerController } from './RunnerController';
 | 
			
		||||
import { RunnerTeamController } from './RunnerTeamController';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@JsonController('/organisations')
 | 
			
		||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
export class RunnerOrganisationController {
 | 
			
		||||
	private runnerOrganisationRepository: Repository<RunnerOrganisation>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Gets the repository of this controller's model/entity.
 | 
			
		||||
	 */
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.runnerOrganisationRepository = getConnectionManager().get().getRepository(RunnerOrganisation);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get()
 | 
			
		||||
	@Authorized("ORGANISATION:GET")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerOrganisation, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all organisations. <br> This includes their address, contact and teams (if existing/associated).' })
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseTeams: ResponseRunnerOrganisation[] = new Array<ResponseRunnerOrganisation>();
 | 
			
		||||
		const runners = await this.runnerOrganisationRepository.find({ relations: ['address', 'contact', 'teams'] });
 | 
			
		||||
		runners.forEach(runner => {
 | 
			
		||||
			responseTeams.push(new ResponseRunnerOrganisation(runner));
 | 
			
		||||
		});
 | 
			
		||||
		return responseTeams;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id')
 | 
			
		||||
	@Authorized("ORGANISATION:GET")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerOrganisation)
 | 
			
		||||
	@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(RunnerOrganisationNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about the organisation whose id got provided.' })
 | 
			
		||||
	async getOne(@Param('id') id: number) {
 | 
			
		||||
		let runnerOrg = await this.runnerOrganisationRepository.findOne({ id: id }, { relations: ['address', 'contact', 'teams'] });
 | 
			
		||||
		if (!runnerOrg) { throw new RunnerOrganisationNotFoundError(); }
 | 
			
		||||
		return new ResponseRunnerOrganisation(runnerOrg);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post()
 | 
			
		||||
	@Authorized("ORGANISATION:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerOrganisation)
 | 
			
		||||
	@OpenAPI({ description: 'Create a new organsisation.' })
 | 
			
		||||
	async post(@Body({ validate: true }) createRunnerOrganisation: CreateRunnerOrganisation) {
 | 
			
		||||
		let runnerOrganisation;
 | 
			
		||||
		try {
 | 
			
		||||
			runnerOrganisation = await createRunnerOrganisation.toRunnerOrganisation();
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		runnerOrganisation = await this.runnerOrganisationRepository.save(runnerOrganisation);
 | 
			
		||||
 | 
			
		||||
		return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne(runnerOrganisation, { relations: ['address', 'contact', 'teams'] }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/:id')
 | 
			
		||||
	@Authorized("ORGANISATION:UPDATE")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerOrganisation)
 | 
			
		||||
	@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(RunnerOrganisationIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Update the organisation whose id you provided. <br> Please remember that ids can't be changed." })
 | 
			
		||||
	async put(@Param('id') id: number, @Body({ validate: true }) updateOrganisation: UpdateRunnerOrganisation) {
 | 
			
		||||
		let oldRunnerOrganisation = await this.runnerOrganisationRepository.findOne({ id: id });
 | 
			
		||||
 | 
			
		||||
		if (!oldRunnerOrganisation) {
 | 
			
		||||
			throw new RunnerOrganisationNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldRunnerOrganisation.id != updateOrganisation.id) {
 | 
			
		||||
			throw new RunnerOrganisationIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.runnerOrganisationRepository.save(await updateOrganisation.updateRunnerOrganisation(oldRunnerOrganisation));
 | 
			
		||||
 | 
			
		||||
		return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne(id, { relations: ['address', 'contact', 'teams'] }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
	@Authorized("ORGANISATION:DELETE")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerOrganisation)
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@ResponseSchema(RunnerOrganisationHasTeamsError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(RunnerOrganisationHasRunnersError, { statusCode: 406 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: 'Delete the organsisation whose id you provided. <br> If the organisation still has runners and/or teams associated this will fail. <br> To delete the organisation with all associated runners and teams set the force QueryParam to true (cascading deletion might take a while). <br> If no organisation with this id exists it will just return 204(no content).' })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let organisation = await this.runnerOrganisationRepository.findOne({ id: id });
 | 
			
		||||
		if (!organisation) { return null; }
 | 
			
		||||
		let runnerOrganisation = await this.runnerOrganisationRepository.findOne(organisation, { relations: ['address', 'contact', 'runners', 'teams'] });
 | 
			
		||||
 | 
			
		||||
		if (!force) {
 | 
			
		||||
			if (runnerOrganisation.teams.length != 0) {
 | 
			
		||||
				throw new RunnerOrganisationHasTeamsError();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		const teamController = new RunnerTeamController()
 | 
			
		||||
		for (let team of runnerOrganisation.teams) {
 | 
			
		||||
			await teamController.remove(team.id, true);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!force) {
 | 
			
		||||
			if (runnerOrganisation.runners.length != 0) {
 | 
			
		||||
				throw new RunnerOrganisationHasRunnersError();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		const runnerController = new RunnerController()
 | 
			
		||||
		for (let runner of runnerOrganisation.runners) {
 | 
			
		||||
			await runnerController.remove(runner.id, true);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const responseOrganisation = new ResponseRunnerOrganisation(runnerOrganisation);
 | 
			
		||||
		await this.runnerOrganisationRepository.delete(organisation);
 | 
			
		||||
		return responseOrganisation;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										145
									
								
								src/controllers/RunnerOrganizationController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/controllers/RunnerOrganizationController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,145 @@
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { RunnerOrganizationHasRunnersError, RunnerOrganizationHasTeamsError, RunnerOrganizationIdsNotMatchingError, RunnerOrganizationNotFoundError } from '../errors/RunnerOrganizationErrors';
 | 
			
		||||
import { CreateRunnerOrganization } from '../models/actions/create/CreateRunnerOrganization';
 | 
			
		||||
import { UpdateRunnerOrganization } from '../models/actions/update/UpdateRunnerOrganization';
 | 
			
		||||
import { Runner } from '../models/entities/Runner';
 | 
			
		||||
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseRunner } from '../models/responses/ResponseRunner';
 | 
			
		||||
import { ResponseRunnerOrganization } from '../models/responses/ResponseRunnerOrganization';
 | 
			
		||||
import { RunnerController } from './RunnerController';
 | 
			
		||||
import { RunnerTeamController } from './RunnerTeamController';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@JsonController('/organizations')
 | 
			
		||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
export class RunnerOrganizationController {
 | 
			
		||||
	private runnerOrganizationRepository: Repository<RunnerOrganization>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Gets the repository of this controller's model/entity.
 | 
			
		||||
	 */
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.runnerOrganizationRepository = getConnectionManager().get().getRepository(RunnerOrganization);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get()
 | 
			
		||||
	@Authorized("ORGANIZATION:GET")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerOrganization, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all organizations. <br> This includes their address, contact and teams (if existing/associated).' })
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseTeams: ResponseRunnerOrganization[] = new Array<ResponseRunnerOrganization>();
 | 
			
		||||
		const runners = await this.runnerOrganizationRepository.find({ relations: ['contact', 'teams'] });
 | 
			
		||||
		runners.forEach(runner => {
 | 
			
		||||
			responseTeams.push(new ResponseRunnerOrganization(runner));
 | 
			
		||||
		});
 | 
			
		||||
		return responseTeams;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id')
 | 
			
		||||
	@Authorized("ORGANIZATION:GET")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerOrganization)
 | 
			
		||||
	@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(RunnerOrganizationNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about the organization whose id got provided.' })
 | 
			
		||||
	async getOne(@Param('id') id: number) {
 | 
			
		||||
		let runnerOrg = await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['contact', 'teams'] });
 | 
			
		||||
		if (!runnerOrg) { throw new RunnerOrganizationNotFoundError(); }
 | 
			
		||||
		return new ResponseRunnerOrganization(runnerOrg);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id/runners')
 | 
			
		||||
	@Authorized(["RUNNER:GET", "SCAN:GET"])
 | 
			
		||||
	@ResponseSchema(ResponseRunner, { isArray: true })
 | 
			
		||||
	@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all runners from this org and it\'s teams (if you don\'t provide the ?onlyDirect=true param). <br> This includes the runner\'s group and distance ran.' })
 | 
			
		||||
	async getRunners(@Param('id') id: number, @QueryParam('onlyDirect') onlyDirect: boolean) {
 | 
			
		||||
		let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
 | 
			
		||||
		let runners: Runner[];
 | 
			
		||||
		if (!onlyDirect) { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.group', 'teams.runners.group.parentGroup', 'teams.runners.scans', 'teams.runners.scans.track'] })).allRunners; }
 | 
			
		||||
		else { runners = (await this.runnerOrganizationRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track'] })).runners; }
 | 
			
		||||
		runners.forEach(runner => {
 | 
			
		||||
			responseRunners.push(new ResponseRunner(runner));
 | 
			
		||||
		});
 | 
			
		||||
		return responseRunners;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post()
 | 
			
		||||
	@Authorized("ORGANIZATION:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerOrganization)
 | 
			
		||||
	@OpenAPI({ description: 'Create a new organsisation.' })
 | 
			
		||||
	async post(@Body({ validate: true }) createRunnerOrganization: CreateRunnerOrganization) {
 | 
			
		||||
		let runnerOrganization;
 | 
			
		||||
		try {
 | 
			
		||||
			runnerOrganization = await createRunnerOrganization.toEntity();
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		runnerOrganization = await this.runnerOrganizationRepository.save(runnerOrganization);
 | 
			
		||||
 | 
			
		||||
		return new ResponseRunnerOrganization(await this.runnerOrganizationRepository.findOne(runnerOrganization, { relations: ['contact', 'teams'] }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/:id')
 | 
			
		||||
	@Authorized("ORGANIZATION:UPDATE")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerOrganization)
 | 
			
		||||
	@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(RunnerOrganizationIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Update the organization whose id you provided. <br> Please remember that ids can't be changed." })
 | 
			
		||||
	async put(@Param('id') id: number, @Body({ validate: true }) updateOrganization: UpdateRunnerOrganization) {
 | 
			
		||||
		let oldRunnerOrganization = await this.runnerOrganizationRepository.findOne({ id: id });
 | 
			
		||||
 | 
			
		||||
		if (!oldRunnerOrganization) {
 | 
			
		||||
			throw new RunnerOrganizationNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldRunnerOrganization.id != updateOrganization.id) {
 | 
			
		||||
			throw new RunnerOrganizationIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.runnerOrganizationRepository.save(await updateOrganization.update(oldRunnerOrganization));
 | 
			
		||||
 | 
			
		||||
		return new ResponseRunnerOrganization(await this.runnerOrganizationRepository.findOne(id, { relations: ['contact', 'teams'] }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
	@Authorized("ORGANIZATION:DELETE")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerOrganization)
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@ResponseSchema(RunnerOrganizationHasTeamsError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(RunnerOrganizationHasRunnersError, { statusCode: 406 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: 'Delete the organsisation whose id you provided. <br> If the organization still has runners and/or teams associated this will fail. <br> To delete the organization with all associated runners and teams set the force QueryParam to true (cascading deletion might take a while). <br> This won\'t delete the associated contact. <br> If no organization with this id exists it will just return 204(no content).' })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let organization = await this.runnerOrganizationRepository.findOne({ id: id });
 | 
			
		||||
		if (!organization) { return null; }
 | 
			
		||||
		let runnerOrganization = await this.runnerOrganizationRepository.findOne(organization, { relations: ['contact', 'runners', 'teams'] });
 | 
			
		||||
 | 
			
		||||
		if (!force) {
 | 
			
		||||
			if (runnerOrganization.teams.length != 0) {
 | 
			
		||||
				throw new RunnerOrganizationHasTeamsError();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		const teamController = new RunnerTeamController()
 | 
			
		||||
		for (let team of runnerOrganization.teams) {
 | 
			
		||||
			await teamController.remove(team.id, true);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!force) {
 | 
			
		||||
			if (runnerOrganization.runners.length != 0) {
 | 
			
		||||
				throw new RunnerOrganizationHasRunnersError();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		const runnerController = new RunnerController()
 | 
			
		||||
		for (let runner of runnerOrganization.runners) {
 | 
			
		||||
			await runnerController.remove(runner.id, true);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const responseOrganization = new ResponseRunnerOrganization(runnerOrganization);
 | 
			
		||||
		await this.runnerOrganizationRepository.delete(organization);
 | 
			
		||||
		return responseOrganization;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										228
									
								
								src/controllers/RunnerSelfServiceController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								src/controllers/RunnerSelfServiceController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,228 @@
 | 
			
		||||
import { Request } from "express";
 | 
			
		||||
import * as jwt from "jsonwebtoken";
 | 
			
		||||
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { config } from '../config';
 | 
			
		||||
import { InvalidCredentialsError, JwtNotProvidedError } from '../errors/AuthError';
 | 
			
		||||
import { MailSendingError } from '../errors/MailErrors';
 | 
			
		||||
import { RunnerEmailNeededError, RunnerHasDistanceDonationsError, RunnerNotFoundError, RunnerSelfserviceTimeoutError } from '../errors/RunnerErrors';
 | 
			
		||||
import { RunnerOrganizationNotFoundError } from '../errors/RunnerOrganizationErrors';
 | 
			
		||||
import { ScanStationNotFoundError } from '../errors/ScanStationErrors';
 | 
			
		||||
import { JwtCreator } from '../jwtcreator';
 | 
			
		||||
import { Mailer } from '../mailer';
 | 
			
		||||
import ScanAuth from '../middlewares/ScanAuth';
 | 
			
		||||
import { CreateSelfServiceCitizenRunner } from '../models/actions/create/CreateSelfServiceCitizenRunner';
 | 
			
		||||
import { CreateSelfServiceRunner } from '../models/actions/create/CreateSelfServiceRunner';
 | 
			
		||||
import { Runner } from '../models/entities/Runner';
 | 
			
		||||
import { RunnerGroup } from '../models/entities/RunnerGroup';
 | 
			
		||||
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
 | 
			
		||||
import { ScanStation } from '../models/entities/ScanStation';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseScanStation } from '../models/responses/ResponseScanStation';
 | 
			
		||||
import { ResponseSelfServiceOrganisation } from '../models/responses/ResponseSelfServiceOrganisation';
 | 
			
		||||
import { ResponseSelfServiceRunner } from '../models/responses/ResponseSelfServiceRunner';
 | 
			
		||||
import { ResponseSelfServiceScan } from '../models/responses/ResponseSelfServiceScan';
 | 
			
		||||
import { DonationController } from './DonationController';
 | 
			
		||||
import { RunnerCardController } from './RunnerCardController';
 | 
			
		||||
import { ScanController } from './ScanController';
 | 
			
		||||
 | 
			
		||||
@JsonController()
 | 
			
		||||
export class RunnerSelfServiceController {
 | 
			
		||||
	private runnerRepository: Repository<Runner>;
 | 
			
		||||
	private orgRepository: Repository<RunnerOrganization>;
 | 
			
		||||
	private stationRepository: Repository<ScanStation>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Gets the repository of this controller's model/entity.
 | 
			
		||||
	 */
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.runnerRepository = getConnectionManager().get().getRepository(Runner);
 | 
			
		||||
		this.orgRepository = getConnectionManager().get().getRepository(RunnerOrganization);
 | 
			
		||||
		this.stationRepository = getConnectionManager().get().getRepository(ScanStation);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/runners/me/:jwt')
 | 
			
		||||
	@ResponseSchema(ResponseSelfServiceRunner)
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(RunnerNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about yourself. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please use the forgot endpoint.' })
 | 
			
		||||
	async get(@Param('jwt') token: string) {
 | 
			
		||||
		return (new ResponseSelfServiceRunner(await this.getRunner(token)));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/runners/me/:jwt')
 | 
			
		||||
	@ResponseSchema(ResponseSelfServiceRunner)
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(RunnerNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Deletes all information about yourself. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please use the forgot endpoint.' })
 | 
			
		||||
	async remove(@Param('jwt') token: string, @QueryParam("force") force: boolean) {
 | 
			
		||||
		const responseRunner = await this.getRunner(token);
 | 
			
		||||
		let runner = await this.runnerRepository.findOne({ id: responseRunner.id });
 | 
			
		||||
 | 
			
		||||
		if (!runner) { return null; }
 | 
			
		||||
		if (!runner) {
 | 
			
		||||
			throw new RunnerNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const runnerDonations = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["distanceDonations"] })).distanceDonations;
 | 
			
		||||
		if (runnerDonations.length > 0 && !force) {
 | 
			
		||||
			throw new RunnerHasDistanceDonationsError();
 | 
			
		||||
		}
 | 
			
		||||
		const donationController = new DonationController();
 | 
			
		||||
		for (let donation of runnerDonations) {
 | 
			
		||||
			await donationController.remove(donation.id, force);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards;
 | 
			
		||||
		const cardController = new RunnerCardController;
 | 
			
		||||
		for (let card of runnerCards) {
 | 
			
		||||
			await cardController.remove(card.id, force);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans;
 | 
			
		||||
		const scanController = new ScanController;
 | 
			
		||||
		for (let scan of runnerScans) {
 | 
			
		||||
			await scanController.remove(scan.id, force);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.runnerRepository.delete(runner);
 | 
			
		||||
		return new ResponseSelfServiceRunner(responseRunner);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/runners/me/:jwt/scans')
 | 
			
		||||
	@ResponseSchema(ResponseSelfServiceScan, { isArray: true })
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(RunnerNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all your (runner) scans. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please contact support.' })
 | 
			
		||||
	async getScans(@Param('jwt') token: string) {
 | 
			
		||||
		const scans = (await this.getRunner(token)).scans;
 | 
			
		||||
		let responseScans = new Array<ResponseSelfServiceScan>()
 | 
			
		||||
		for (let scan of scans) {
 | 
			
		||||
			responseScans.push(new ResponseSelfServiceScan(scan));
 | 
			
		||||
		}
 | 
			
		||||
		return responseScans;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/stations/me')
 | 
			
		||||
	@UseBefore(ScanAuth)
 | 
			
		||||
	@ResponseSchema(ResponseScanStation)
 | 
			
		||||
	@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(ScanStationNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists basic information about the station whose token got provided. <br> This includes it\'s associated track.', security: [{ "StationApiToken": [] }] })
 | 
			
		||||
	async getStationMe(@Req() req: Request) {
 | 
			
		||||
		let scan = await this.stationRepository.findOne({ id: parseInt(req.headers["station_id"].toString()) }, { relations: ['track'] })
 | 
			
		||||
		if (!scan) { throw new ScanStationNotFoundError(); }
 | 
			
		||||
		return scan.toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post('/runners/forgot')
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(ResponseEmpty)
 | 
			
		||||
	@OpenAPI({ description: 'Use this endpoint to reuqest a new selfservice token/link to be sent to your mail address (rate limited to one mail every 24hrs).' })
 | 
			
		||||
	async requestNewToken(@QueryParam('mail') mail: string, @QueryParam("locale") locale: string = "en") {
 | 
			
		||||
		if (!mail) {
 | 
			
		||||
			throw new RunnerNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
		const runner = await this.runnerRepository.findOne({ email: mail });
 | 
			
		||||
		if (!runner) { throw new RunnerNotFoundError(); }
 | 
			
		||||
 | 
			
		||||
		if (runner.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 60 * 60 * 24)) { throw new RunnerSelfserviceTimeoutError(); }
 | 
			
		||||
		const token = JwtCreator.createSelfService(runner);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			await Mailer.sendSelfserviceForgottenMail(runner.email, token, locale)
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw new MailSendingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		runner.resetRequestedTimestamp = Math.floor(Date.now() / 1000);
 | 
			
		||||
		await this.runnerRepository.save(runner);
 | 
			
		||||
 | 
			
		||||
		return { token };
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post('/runners/register')
 | 
			
		||||
	@ResponseSchema(ResponseSelfServiceRunner)
 | 
			
		||||
	@ResponseSchema(RunnerEmailNeededError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: 'Create a new selfservice runner in the citizen org. <br> This endpoint shoud be used to allow "everyday citizen" to register themselves. <br> You have to provide a mail address, b/c the future we\'ll implement email verification.' })
 | 
			
		||||
	async registerRunner(@Body({ validate: true }) createRunner: CreateSelfServiceCitizenRunner, @QueryParam("locale") locale: string = "en") {
 | 
			
		||||
		let runner = await createRunner.toEntity();
 | 
			
		||||
 | 
			
		||||
		runner = await this.runnerRepository.save(runner);
 | 
			
		||||
		let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }));
 | 
			
		||||
		response.token = JwtCreator.createSelfService(runner);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			await Mailer.sendSelfserviceWelcomeMail(runner.email, response.token, locale)
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw new MailSendingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return response;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post('/runners/register/:token')
 | 
			
		||||
	@ResponseSchema(ResponseSelfServiceRunner)
 | 
			
		||||
	@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: 'Create a new selfservice runner in a provided org. <br> The orgs get provided and authorized via api tokens that can be optained via the /organizations endpoint.' })
 | 
			
		||||
	async registerOrganizationRunner(@Param('token') token: string, @Body({ validate: true }) createRunner: CreateSelfServiceRunner, @QueryParam("locale") locale: string = "en") {
 | 
			
		||||
		const org = await this.getOrgansisation(token);
 | 
			
		||||
 | 
			
		||||
		let runner = await createRunner.toEntity(org);
 | 
			
		||||
		runner = await this.runnerRepository.save(runner);
 | 
			
		||||
 | 
			
		||||
		let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }));
 | 
			
		||||
		response.token = JwtCreator.createSelfService(runner);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			await Mailer.sendSelfserviceWelcomeMail(runner.email, response.token, locale)
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw new MailSendingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return response;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/organizations/selfservice/:token')
 | 
			
		||||
	@ResponseSchema(ResponseSelfServiceOrganisation, { isArray: false })
 | 
			
		||||
	@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: 'Get the basic info and teams for a org.' })
 | 
			
		||||
	async getSelfserviceOrg(@Param('token') token: string) {
 | 
			
		||||
		const orgid = (await this.getOrgansisation(token)).id;
 | 
			
		||||
		const org = await this.orgRepository.findOne({ id: orgid }, { relations: ['teams'] })
 | 
			
		||||
 | 
			
		||||
		return new ResponseSelfServiceOrganisation(<RunnerOrganization>org);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Get's a runner by a provided jwt token.
 | 
			
		||||
	 * @param token The runner jwt provided by the runner to identitfy themselves.
 | 
			
		||||
	 */
 | 
			
		||||
	private async getRunner(token: string): Promise<Runner> {
 | 
			
		||||
		if (token == "") { throw new JwtNotProvidedError(); }
 | 
			
		||||
		let jwtPayload = undefined
 | 
			
		||||
		try {
 | 
			
		||||
			jwtPayload = <any>jwt.verify(token, config.jwt_secret);
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw new InvalidCredentialsError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const runner = await this.runnerRepository.findOne({ id: jwtPayload["id"] }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] });
 | 
			
		||||
		if (!runner) { throw new RunnerNotFoundError() }
 | 
			
		||||
		return runner;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Get's a runner org by a provided registration api key.
 | 
			
		||||
	 * @param token The organization's registration api token.
 | 
			
		||||
	 */
 | 
			
		||||
	private async getOrgansisation(token: string): Promise<RunnerGroup> {
 | 
			
		||||
		token = Buffer.from(token, 'base64').toString('utf8');
 | 
			
		||||
 | 
			
		||||
		const organization = await this.orgRepository.findOne({ key: token });
 | 
			
		||||
		if (!organization) { throw new RunnerOrganizationNotFoundError; }
 | 
			
		||||
 | 
			
		||||
		return organization;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -2,10 +2,11 @@ import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { RunnerTeamHasRunnersError, RunnerTeamIdsNotMatchingError, RunnerTeamNotFoundError } from '../errors/RunnerTeamErrors';
 | 
			
		||||
import { CreateRunnerTeam } from '../models/actions/CreateRunnerTeam';
 | 
			
		||||
import { UpdateRunnerTeam } from '../models/actions/UpdateRunnerTeam';
 | 
			
		||||
import { CreateRunnerTeam } from '../models/actions/create/CreateRunnerTeam';
 | 
			
		||||
import { UpdateRunnerTeam } from '../models/actions/update/UpdateRunnerTeam';
 | 
			
		||||
import { RunnerTeam } from '../models/entities/RunnerTeam';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseRunner } from '../models/responses/ResponseRunner';
 | 
			
		||||
import { ResponseRunnerTeam } from '../models/responses/ResponseRunnerTeam';
 | 
			
		||||
import { RunnerController } from './RunnerController';
 | 
			
		||||
 | 
			
		||||
@@ -25,7 +26,7 @@ export class RunnerTeamController {
 | 
			
		||||
	@Get()
 | 
			
		||||
	@Authorized("TEAM:GET")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerTeam, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all teams. <br> This includes their parent organisation and contact (if existing/associated).' })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all teams. <br> This includes their parent organization and contact (if existing/associated).' })
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseTeams: ResponseRunnerTeam[] = new Array<ResponseRunnerTeam>();
 | 
			
		||||
		const runners = await this.runnerTeamRepository.find({ relations: ['parentGroup', 'contact'] });
 | 
			
		||||
@@ -47,6 +48,20 @@ export class RunnerTeamController {
 | 
			
		||||
		return new ResponseRunnerTeam(runnerTeam);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id/runners')
 | 
			
		||||
	@Authorized(["RUNNER:GET", "SCAN:GET"])
 | 
			
		||||
	@ResponseSchema(ResponseRunner, { isArray: true })
 | 
			
		||||
	@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all runners from this team. <br> This includes the runner\'s group and distance ran.' })
 | 
			
		||||
	async getRunners(@Param('id') id: number) {
 | 
			
		||||
		let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
 | 
			
		||||
		const runners = (await this.runnerTeamRepository.findOne({ id: id }, { relations: ['runners', 'runners.group', 'runners.group.parentGroup', 'runners.scans', 'runners.scans.track'] })).runners;
 | 
			
		||||
		runners.forEach(runner => {
 | 
			
		||||
			responseRunners.push(new ResponseRunner(runner));
 | 
			
		||||
		});
 | 
			
		||||
		return responseRunners;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post()
 | 
			
		||||
	@Authorized("TEAM:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseRunnerTeam)
 | 
			
		||||
@@ -54,7 +69,7 @@ export class RunnerTeamController {
 | 
			
		||||
	async post(@Body({ validate: true }) createRunnerTeam: CreateRunnerTeam) {
 | 
			
		||||
		let runnerTeam;
 | 
			
		||||
		try {
 | 
			
		||||
			runnerTeam = await createRunnerTeam.toRunnerTeam();
 | 
			
		||||
			runnerTeam = await createRunnerTeam.toEntity();
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
@@ -82,7 +97,7 @@ export class RunnerTeamController {
 | 
			
		||||
			throw new RunnerTeamIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.runnerTeamRepository.save(await runnerTeam.updateRunnerTeam(oldRunnerTeam));
 | 
			
		||||
		await this.runnerTeamRepository.save(await runnerTeam.update(oldRunnerTeam));
 | 
			
		||||
 | 
			
		||||
		return new ResponseRunnerTeam(await this.runnerTeamRepository.findOne({ id: runnerTeam.id }, { relations: ['parentGroup', 'contact'] }));
 | 
			
		||||
	}
 | 
			
		||||
@@ -93,7 +108,7 @@ export class RunnerTeamController {
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@ResponseSchema(RunnerTeamHasRunnersError, { statusCode: 406 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: 'Delete the team whose id you provided. <br> If the team still has runners associated this will fail. <br> To delete the team with all associated runners set the force QueryParam to true (cascading deletion might take a while). <br> If no team with this id exists it will just return 204(no content).' })
 | 
			
		||||
	@OpenAPI({ description: 'Delete the team whose id you provided. <br> If the team still has runners associated this will fail. <br> To delete the team with all associated runners set the force QueryParam to true (cascading deletion might take a while). <br> This won\'t delete the associated contact.<br> If no team with this id exists it will just return 204(no content).' })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let team = await this.runnerTeamRepository.findOne({ id: id });
 | 
			
		||||
		if (!team) { return null; }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										144
									
								
								src/controllers/ScanController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/controllers/ScanController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
import { Request } from "express";
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam, Req, UseBefore } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { RunnerNotFoundError } from '../errors/RunnerErrors';
 | 
			
		||||
import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors';
 | 
			
		||||
import { ScanStationNotFoundError } from '../errors/ScanStationErrors';
 | 
			
		||||
import ScanAuth from '../middlewares/ScanAuth';
 | 
			
		||||
import { CreateScan } from '../models/actions/create/CreateScan';
 | 
			
		||||
import { CreateTrackScan } from '../models/actions/create/CreateTrackScan';
 | 
			
		||||
import { UpdateScan } from '../models/actions/update/UpdateScan';
 | 
			
		||||
import { UpdateTrackScan } from '../models/actions/update/UpdateTrackScan';
 | 
			
		||||
import { Scan } from '../models/entities/Scan';
 | 
			
		||||
import { TrackScan } from '../models/entities/TrackScan';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseScan } from '../models/responses/ResponseScan';
 | 
			
		||||
import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
 | 
			
		||||
@JsonController('/scans')
 | 
			
		||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
export class ScanController {
 | 
			
		||||
	private scanRepository: Repository<Scan>;
 | 
			
		||||
	private trackScanRepository: Repository<TrackScan>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Gets the repository of this controller's model/entity.
 | 
			
		||||
	 */
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.scanRepository = getConnectionManager().get().getRepository(Scan);
 | 
			
		||||
		this.trackScanRepository = getConnectionManager().get().getRepository(TrackScan);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get()
 | 
			
		||||
	@Authorized("SCAN:GET")
 | 
			
		||||
	@ResponseSchema(ResponseScan, { isArray: true })
 | 
			
		||||
	@ResponseSchema(ResponseTrackScan, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all scans (normal or track) from all runners. <br> This includes the scan\'s runner\'s distance ran.' })
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseScans: ResponseScan[] = new Array<ResponseScan>();
 | 
			
		||||
		const scans = await this.scanRepository.find({ relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] });
 | 
			
		||||
		scans.forEach(scan => {
 | 
			
		||||
			responseScans.push(scan.toResponse());
 | 
			
		||||
		});
 | 
			
		||||
		return responseScans;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id')
 | 
			
		||||
	@Authorized("SCAN:GET")
 | 
			
		||||
	@ResponseSchema(ResponseScan)
 | 
			
		||||
	@ResponseSchema(ResponseTrackScan)
 | 
			
		||||
	@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(ScanNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about the scan whose id got provided. This includes the scan\'s runner\'s distance ran.' })
 | 
			
		||||
	async getOne(@Param('id') id: number) {
 | 
			
		||||
		let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })
 | 
			
		||||
		if (!scan) { throw new ScanNotFoundError(); }
 | 
			
		||||
		return scan.toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post()
 | 
			
		||||
	@UseBefore(ScanAuth)
 | 
			
		||||
	@ResponseSchema(ResponseScan)
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: 'Create a new scan (not track scan - use /scans/trackscans instead). <br> Please rmemember to provide the scan\'s runner\'s id and distance.', security: [{ "StationApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
	async post(@Body({ validate: true }) createScan: CreateScan) {
 | 
			
		||||
		let scan = await createScan.toEntity();
 | 
			
		||||
		scan = await this.scanRepository.save(scan);
 | 
			
		||||
		return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post("/trackscans")
 | 
			
		||||
	@UseBefore(ScanAuth)
 | 
			
		||||
	@ResponseSchema(ResponseTrackScan)
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: 'Create a new track scan (for "normal" scans use /scans instead). <br> Please remember that to provide the scan\'s card\'s station\'s id.', security: [{ "StationApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
	async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan, @Req() req: Request) {
 | 
			
		||||
		const station_id = req.headers["station_id"];
 | 
			
		||||
		if (station_id) {
 | 
			
		||||
			createScan.station = parseInt(station_id.toString());
 | 
			
		||||
		}
 | 
			
		||||
		let scan = await createScan.toEntity();
 | 
			
		||||
		scan = await this.trackScanRepository.save(scan);
 | 
			
		||||
		return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/:id')
 | 
			
		||||
	@Authorized("SCAN:UPDATE")
 | 
			
		||||
	@ResponseSchema(ResponseScan)
 | 
			
		||||
	@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Update the scan (not track scan use /scans/trackscans/:id instead) whose id you provided. <br> Please remember that ids can't be changed and distances must be positive." })
 | 
			
		||||
	async put(@Param('id') id: number, @Body({ validate: true }) scan: UpdateScan) {
 | 
			
		||||
		let oldScan = await this.scanRepository.findOne({ id: id });
 | 
			
		||||
 | 
			
		||||
		if (!oldScan) {
 | 
			
		||||
			throw new ScanNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldScan.id != scan.id) {
 | 
			
		||||
			throw new ScanIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.scanRepository.save(await scan.update(oldScan));
 | 
			
		||||
		return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/trackscans/:id')
 | 
			
		||||
	@Authorized("SCAN:UPDATE")
 | 
			
		||||
	@ResponseSchema(ResponseTrackScan)
 | 
			
		||||
	@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: 'Update the track scan (not "normal" scan use /scans/trackscans/:id instead) whose id you provided. <br> Please remember that only the validity, runner and track can be changed.' })
 | 
			
		||||
	async putTrackScan(@Param('id') id: number, @Body({ validate: true }) scan: UpdateTrackScan) {
 | 
			
		||||
		let oldScan = await this.trackScanRepository.findOne({ id: id });
 | 
			
		||||
 | 
			
		||||
		if (!oldScan) {
 | 
			
		||||
			throw new ScanNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldScan.id != scan.id) {
 | 
			
		||||
			throw new ScanIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.trackScanRepository.save(await scan.update(oldScan));
 | 
			
		||||
		return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
	@Authorized("SCAN:DELETE")
 | 
			
		||||
	@ResponseSchema(ResponseScan)
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: 'Delete the scan whose id you provided. <br> If no scan with this id exists it will just return 204(no content).' })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let scan = await this.scanRepository.findOne({ id: id });
 | 
			
		||||
		if (!scan) { return null; }
 | 
			
		||||
		const responseScan = await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track', 'card', 'station'] });
 | 
			
		||||
 | 
			
		||||
		await this.scanRepository.delete(scan);
 | 
			
		||||
		return responseScan.toResponse();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										108
									
								
								src/controllers/ScanStationController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/controllers/ScanStationController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { ScanStationHasScansError, ScanStationIdsNotMatchingError, ScanStationNotFoundError } from '../errors/ScanStationErrors';
 | 
			
		||||
import { TrackNotFoundError } from '../errors/TrackErrors';
 | 
			
		||||
import { CreateScanStation } from '../models/actions/create/CreateScanStation';
 | 
			
		||||
import { UpdateScanStation } from '../models/actions/update/UpdateScanStation';
 | 
			
		||||
import { ScanStation } from '../models/entities/ScanStation';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseScanStation } from '../models/responses/ResponseScanStation';
 | 
			
		||||
import { ScanController } from './ScanController';
 | 
			
		||||
 | 
			
		||||
@JsonController('/stations')
 | 
			
		||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
export class ScanStationController {
 | 
			
		||||
	private stationRepository: Repository<ScanStation>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Gets the repository of this controller's model/entity.
 | 
			
		||||
	 */
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.stationRepository = getConnectionManager().get().getRepository(ScanStation);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get()
 | 
			
		||||
	@Authorized("STATION:GET")
 | 
			
		||||
	@ResponseSchema(ResponseScanStation, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all stations. <br> This includes their associated tracks.' })
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseStations: ResponseScanStation[] = new Array<ResponseScanStation>();
 | 
			
		||||
		const stations = await this.stationRepository.find({ relations: ['track'] });
 | 
			
		||||
		stations.forEach(station => {
 | 
			
		||||
			responseStations.push(station.toResponse());
 | 
			
		||||
		});
 | 
			
		||||
		return responseStations;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id')
 | 
			
		||||
	@Authorized("STATION:GET")
 | 
			
		||||
	@ResponseSchema(ResponseScanStation)
 | 
			
		||||
	@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(ScanStationNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about the station whose id got provided. <br> This includes it\'s associated track.' })
 | 
			
		||||
	async getOne(@Param('id') id: number) {
 | 
			
		||||
		let scan = await this.stationRepository.findOne({ id: id }, { relations: ['track'] })
 | 
			
		||||
		if (!scan) { throw new ScanStationNotFoundError(); }
 | 
			
		||||
		return scan.toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post()
 | 
			
		||||
	@Authorized("STATION:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseScanStation)
 | 
			
		||||
	@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OpenAPI({ description: 'Create a new station. <br> Please remeber to provide the station\'s track\'s id. <br> Please also remember that the station key is only visibe on creation.' })
 | 
			
		||||
	async post(@Body({ validate: true }) createStation: CreateScanStation) {
 | 
			
		||||
		let newStation = await createStation.toEntity();
 | 
			
		||||
		const station = await this.stationRepository.save(newStation);
 | 
			
		||||
		let responseStation = (await this.stationRepository.findOne({ id: station.id }, { relations: ['track'] })).toResponse();
 | 
			
		||||
		responseStation.key = newStation.cleartextkey;
 | 
			
		||||
		return responseStation;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/:id')
 | 
			
		||||
	@Authorized("STATION:UPDATE")
 | 
			
		||||
	@ResponseSchema(ResponseScanStation)
 | 
			
		||||
	@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(ScanStationIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Update the station whose id you provided. <br> Please remember that only the description and enabled state can be changed." })
 | 
			
		||||
	async put(@Param('id') id: number, @Body({ validate: true }) station: UpdateScanStation) {
 | 
			
		||||
		let oldStation = await this.stationRepository.findOne({ id: id });
 | 
			
		||||
 | 
			
		||||
		if (!oldStation) {
 | 
			
		||||
			throw new ScanStationNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldStation.id != station.id) {
 | 
			
		||||
			throw new ScanStationIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.stationRepository.save(await station.update(oldStation));
 | 
			
		||||
		return (await this.stationRepository.findOne({ id: id }, { relations: ['track'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
	@Authorized("STATION:DELETE")
 | 
			
		||||
	@ResponseSchema(ResponseScanStation)
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@ResponseSchema(ScanStationHasScansError, { statusCode: 406 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: 'Delete the station whose id you provided. <br> If no station with this id exists it will just return 204(no content). <br> If the station still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with/created by this station - please disable it instead).' })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let station = await this.stationRepository.findOne({ id: id });
 | 
			
		||||
		if (!station) { return null; }
 | 
			
		||||
 | 
			
		||||
		const stationScans = (await this.stationRepository.findOne({ id: station.id }, { relations: ["scans"] })).scans;
 | 
			
		||||
		if (stationScans.length != 0 && !force) {
 | 
			
		||||
			throw new ScanStationHasScansError();
 | 
			
		||||
		}
 | 
			
		||||
		const scanController = new ScanController;
 | 
			
		||||
		for (let scan of stationScans) {
 | 
			
		||||
			await scanController.remove(scan.id, force);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const responseStation = await this.stationRepository.findOne({ id: station.id }, { relations: ["track"] });
 | 
			
		||||
		await this.stationRepository.delete(station);
 | 
			
		||||
		return responseStation.toResponse();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										75
									
								
								src/controllers/StatsClientController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/controllers/StatsClientController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { StatsClientNotFoundError } from '../errors/StatsClientErrors';
 | 
			
		||||
import { TrackNotFoundError } from "../errors/TrackErrors";
 | 
			
		||||
import { CreateStatsClient } from '../models/actions/create/CreateStatsClient';
 | 
			
		||||
import { StatsClient } from '../models/entities/StatsClient';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseStatsClient } from '../models/responses/ResponseStatsClient';
 | 
			
		||||
 | 
			
		||||
@JsonController('/statsclients')
 | 
			
		||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
export class StatsClientController {
 | 
			
		||||
	private clientRepository: Repository<StatsClient>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Gets the repository of this controller's model/entity.
 | 
			
		||||
	 */
 | 
			
		||||
	constructor() {
 | 
			
		||||
		this.clientRepository = getConnectionManager().get().getRepository(StatsClient);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get()
 | 
			
		||||
	@Authorized("STATSCLIENT:GET")
 | 
			
		||||
	@ResponseSchema(ResponseStatsClient, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all stats clients. Please remember that the key can only be viewed on creation.' })
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseClients: ResponseStatsClient[] = new Array<ResponseStatsClient>();
 | 
			
		||||
		const clients = await this.clientRepository.find();
 | 
			
		||||
		clients.forEach(clients => {
 | 
			
		||||
			responseClients.push(new ResponseStatsClient(clients));
 | 
			
		||||
		});
 | 
			
		||||
		return responseClients;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id')
 | 
			
		||||
	@Authorized("STATSCLIENT:GET")
 | 
			
		||||
	@ResponseSchema(ResponseStatsClient)
 | 
			
		||||
	@ResponseSchema(StatsClientNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(StatsClientNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: "Lists all information about the stats client whose id got provided. Please remember that the key can only be viewed on creation." })
 | 
			
		||||
	async getOne(@Param('id') id: number) {
 | 
			
		||||
		let client = await this.clientRepository.findOne({ id: id });
 | 
			
		||||
		if (!client) { throw new TrackNotFoundError(); }
 | 
			
		||||
		return new ResponseStatsClient(client);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post()
 | 
			
		||||
	@Authorized("STATSCLIENT:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseStatsClient)
 | 
			
		||||
	@OpenAPI({ description: "Create a new stats client. <br> Please remember that the client\'s key will be generated automaticly and that it can only be viewed on creation." })
 | 
			
		||||
	async post(
 | 
			
		||||
		@Body({ validate: true })
 | 
			
		||||
		client: CreateStatsClient
 | 
			
		||||
	) {
 | 
			
		||||
		let newClient = await this.clientRepository.save(await client.toEntity());
 | 
			
		||||
		let responseClient = new ResponseStatsClient(newClient);
 | 
			
		||||
		responseClient.key = newClient.cleartextkey;
 | 
			
		||||
		return responseClient;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
	@Authorized("STATSCLIENT:DELETE")
 | 
			
		||||
	@ResponseSchema(ResponseStatsClient)
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: "Delete the stats client whose id you provided. <br> If no client with this id exists it will just return 204(no content)." })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let client = await this.clientRepository.findOne({ id: id });
 | 
			
		||||
		if (!client) { return null; }
 | 
			
		||||
 | 
			
		||||
		await this.clientRepository.delete(client);
 | 
			
		||||
		return new ResponseStatsClient(client);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										173
									
								
								src/controllers/StatsController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								src/controllers/StatsController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
			
		||||
import { Get, JsonController, QueryParam, UseBefore } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnection } from 'typeorm';
 | 
			
		||||
import StatsAuth from '../middlewares/StatsAuth';
 | 
			
		||||
import { Donation } from '../models/entities/Donation';
 | 
			
		||||
import { Runner } from '../models/entities/Runner';
 | 
			
		||||
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
 | 
			
		||||
import { RunnerTeam } from '../models/entities/RunnerTeam';
 | 
			
		||||
import { Scan } from '../models/entities/Scan';
 | 
			
		||||
import { TrackScan } from '../models/entities/TrackScan';
 | 
			
		||||
import { User } from '../models/entities/User';
 | 
			
		||||
import { ResponseStats } from '../models/responses/ResponseStats';
 | 
			
		||||
import { ResponseStatsOrgnisation } from '../models/responses/ResponseStatsOrganization';
 | 
			
		||||
import { ResponseStatsRunner } from '../models/responses/ResponseStatsRunner';
 | 
			
		||||
import { ResponseStatsTeam } from '../models/responses/ResponseStatsTeam';
 | 
			
		||||
 | 
			
		||||
@JsonController('/stats')
 | 
			
		||||
export class StatsController {
 | 
			
		||||
 | 
			
		||||
    @Get()
 | 
			
		||||
    @ResponseSchema(ResponseStats)
 | 
			
		||||
    @OpenAPI({ description: "A very basic stats endpoint providing basic counters for a dashboard or simmilar" })
 | 
			
		||||
    async get() {
 | 
			
		||||
        let connection = getConnection();
 | 
			
		||||
        let runners = await connection.getRepository(Runner).find({ relations: ['scans', 'scans.track'] });
 | 
			
		||||
        let teams = await connection.getRepository(RunnerTeam).find();
 | 
			
		||||
        let orgs = await connection.getRepository(RunnerOrganization).find();
 | 
			
		||||
        let users = await connection.getRepository(User).find();
 | 
			
		||||
        let scans = await connection.getRepository(Scan).find();
 | 
			
		||||
        let donations = await connection.getRepository(Donation).find({ relations: ['runner', 'runner.scans', 'runner.scans.track'] });
 | 
			
		||||
        return new ResponseStats(runners, teams, orgs, users, scans, donations)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Get("/runners/distance")
 | 
			
		||||
    @UseBefore(StatsAuth)
 | 
			
		||||
    @ResponseSchema(ResponseStatsRunner, { isArray: true })
 | 
			
		||||
    @OpenAPI({ description: "Returns the top ten runners by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
    async getTopRunnersByDistance() {
 | 
			
		||||
        let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] });
 | 
			
		||||
        if (!runners || runners.length == 0) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        return runners;
 | 
			
		||||
        let topRunners = runners.sort((runner1, runner2) => runner2.distance - runner1.distance).slice(0, 10);
 | 
			
		||||
        return runners;
 | 
			
		||||
        let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
 | 
			
		||||
        topRunners.forEach(runner => {
 | 
			
		||||
            responseRunners.push(new ResponseStatsRunner(runner));
 | 
			
		||||
        });
 | 
			
		||||
        return responseRunners;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Get("/runners/donations")
 | 
			
		||||
    @UseBefore(StatsAuth)
 | 
			
		||||
    @ResponseSchema(ResponseStatsRunner, { isArray: true })
 | 
			
		||||
    @OpenAPI({ description: "Returns the top ten runners by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
    async getTopRunnersByDonations() {
 | 
			
		||||
        let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] });
 | 
			
		||||
        if (!runners || runners.length == 0) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        let topRunners = runners.sort((runner1, runner2) => runner2.distanceDonationAmount - runner1.distanceDonationAmount).slice(0, 10);
 | 
			
		||||
        let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
 | 
			
		||||
        topRunners.forEach(runner => {
 | 
			
		||||
            responseRunners.push(new ResponseStatsRunner(runner));
 | 
			
		||||
        });
 | 
			
		||||
        return responseRunners;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Get("/runners/laptime")
 | 
			
		||||
    @UseBefore(StatsAuth)
 | 
			
		||||
    @ResponseSchema(ResponseStatsRunner, { isArray: true })
 | 
			
		||||
    @OpenAPI({ description: "Returns the top ten runners by fastest laptime on your selected track (track by id).", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
    async getTopRunnersByLaptime(@QueryParam("track") track: number) {
 | 
			
		||||
        let scans = await getConnection().getRepository(TrackScan).find({ relations: ['track', 'runner', 'runner.group', 'runner.scans', 'runner.scans.track', 'runner.distanceDonations'] });
 | 
			
		||||
        if (!scans || scans.length == 0) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        scans = scans.filter((s) => { return s.track.id == track && s.valid == true && s.lapTime != 0 }).sort((scan1, scan2) => scan1.lapTime - scan2.lapTime);
 | 
			
		||||
 | 
			
		||||
        let topScans = new Array<TrackScan>();
 | 
			
		||||
        let knownRunners = new Array<number>();
 | 
			
		||||
        for (let i = 0; i < scans.length && topScans.length < 10; i++) {
 | 
			
		||||
            const element = scans[i];
 | 
			
		||||
            if (!knownRunners.includes(element.runner.id)) {
 | 
			
		||||
                topScans.push(element);
 | 
			
		||||
                knownRunners.push(element.runner.id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
 | 
			
		||||
        topScans.forEach(scan => {
 | 
			
		||||
            responseRunners.push(new ResponseStatsRunner(scan.runner, scan.lapTime));
 | 
			
		||||
        });
 | 
			
		||||
        return responseRunners;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Get("/scans")
 | 
			
		||||
    @UseBefore(StatsAuth)
 | 
			
		||||
    @ResponseSchema(ResponseStatsRunner, { isArray: true })
 | 
			
		||||
    @OpenAPI({ description: "Returns the top ten fastest track times (with their runner and the runner's group).", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
    async getTopRunnersByTrackTime() {
 | 
			
		||||
        throw new Error("Not implemented yet.")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Get("/teams/distance")
 | 
			
		||||
    @UseBefore(StatsAuth)
 | 
			
		||||
    @ResponseSchema(ResponseStatsTeam, { isArray: true })
 | 
			
		||||
    @OpenAPI({ description: "Returns the top ten teams by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
    async getTopTeamsByDistance() {
 | 
			
		||||
        let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
 | 
			
		||||
        if (!teams || teams.length == 0) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        let topTeams = teams.sort((team1, team2) => team2.distance - team1.distance).slice(0, 10);
 | 
			
		||||
        let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
 | 
			
		||||
        topTeams.forEach(team => {
 | 
			
		||||
            responseTeams.push(new ResponseStatsTeam(team));
 | 
			
		||||
        });
 | 
			
		||||
        return responseTeams;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Get("/teams/donations")
 | 
			
		||||
    @UseBefore(StatsAuth)
 | 
			
		||||
    @ResponseSchema(ResponseStatsTeam, { isArray: true })
 | 
			
		||||
    @OpenAPI({ description: "Returns the top ten teams by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
    async getTopTeamsByDonations() {
 | 
			
		||||
        let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
 | 
			
		||||
        if (!teams || teams.length == 0) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        let topTeams = teams.sort((team1, team2) => team2.distanceDonationAmount - team1.distanceDonationAmount).slice(0, 10);
 | 
			
		||||
        let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
 | 
			
		||||
        topTeams.forEach(team => {
 | 
			
		||||
            responseTeams.push(new ResponseStatsTeam(team));
 | 
			
		||||
        });
 | 
			
		||||
        return responseTeams;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Get("/organizations/distance")
 | 
			
		||||
    @UseBefore(StatsAuth)
 | 
			
		||||
    @ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
 | 
			
		||||
    @OpenAPI({ description: "Returns the top ten organizations by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
    async getTopOrgsByDistance() {
 | 
			
		||||
        let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
 | 
			
		||||
        if (!orgs || orgs.length == 0) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        let topOrgs = orgs.sort((org1, org2) => org2.distance - org1.distance).slice(0, 10);
 | 
			
		||||
        let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
 | 
			
		||||
        topOrgs.forEach(org => {
 | 
			
		||||
            responseOrgs.push(new ResponseStatsOrgnisation(org));
 | 
			
		||||
        });
 | 
			
		||||
        return responseOrgs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Get("/organizations/donations")
 | 
			
		||||
    @UseBefore(StatsAuth)
 | 
			
		||||
    @ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
 | 
			
		||||
    @OpenAPI({ description: "Returns the top ten organizations by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
    async getTopOrgsByDonations() {
 | 
			
		||||
        let orgs = await getConnection().getRepository(RunnerOrganization).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
 | 
			
		||||
        if (!orgs || orgs.length == 0) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        let topOrgs = orgs.sort((org1, org2) => org2.distanceDonationAmount - org1.distanceDonationAmount).slice(0, 10);
 | 
			
		||||
        let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
 | 
			
		||||
        topOrgs.forEach(org => {
 | 
			
		||||
            responseOrgs.push(new ResponseStatsOrgnisation(org));
 | 
			
		||||
        });
 | 
			
		||||
        return responseOrgs;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import { Get, JsonController } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnection } from 'typeorm';
 | 
			
		||||
import { config } from '../config';
 | 
			
		||||
 | 
			
		||||
@JsonController('/status')
 | 
			
		||||
@JsonController()
 | 
			
		||||
export class StatusController {
 | 
			
		||||
 | 
			
		||||
    @Get()
 | 
			
		||||
    @Get('/status')
 | 
			
		||||
    @OpenAPI({ description: "A very basic status/health endpoint that just checks if the database connection is available. <br> The available information depth will be expanded later." })
 | 
			
		||||
    get() {
 | 
			
		||||
        let connection;
 | 
			
		||||
@@ -19,4 +20,12 @@ export class StatusController {
 | 
			
		||||
            "database connection": "✔"
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Get('/version')
 | 
			
		||||
    @OpenAPI({ description: "A very basic endpoint that just returns the curent package version." })
 | 
			
		||||
    getVersion() {
 | 
			
		||||
        return {
 | 
			
		||||
            "version": config.version
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { EntityFromBody } from 'typeorm-routing-controllers-extensions';
 | 
			
		||||
import { TrackIdsNotMatchingError, TrackNotFoundError } from "../errors/TrackErrors";
 | 
			
		||||
import { CreateTrack } from '../models/actions/CreateTrack';
 | 
			
		||||
import { TrackHasScanStationsError, TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors";
 | 
			
		||||
import { CreateTrack } from '../models/actions/create/CreateTrack';
 | 
			
		||||
import { UpdateTrack } from '../models/actions/update/UpdateTrack';
 | 
			
		||||
import { Track } from '../models/entities/Track';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseTrack } from '../models/responses/ResponseTrack';
 | 
			
		||||
import { ScanStationController } from './ScanStationController';
 | 
			
		||||
 | 
			
		||||
@JsonController('/tracks')
 | 
			
		||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
 | 
			
		||||
@@ -48,12 +49,13 @@ export class TrackController {
 | 
			
		||||
	@Post()
 | 
			
		||||
	@Authorized("TRACK:CREATE")
 | 
			
		||||
	@ResponseSchema(ResponseTrack)
 | 
			
		||||
	@ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Create a new track. <br> Please remember that the track\'s distance must be greater than 0." })
 | 
			
		||||
	async post(
 | 
			
		||||
		@Body({ validate: true })
 | 
			
		||||
		track: CreateTrack
 | 
			
		||||
	) {
 | 
			
		||||
		return new ResponseTrack(await this.trackRepository.save(track.toTrack()));
 | 
			
		||||
		return new ResponseTrack(await this.trackRepository.save(await track.toEntity()));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/:id')
 | 
			
		||||
@@ -61,20 +63,21 @@ export class TrackController {
 | 
			
		||||
	@ResponseSchema(ResponseTrack)
 | 
			
		||||
	@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(TrackIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Update the track whose id you provided. <br> Please remember that ids can't be changed." })
 | 
			
		||||
	async put(@Param('id') id: number, @EntityFromBody() track: Track) {
 | 
			
		||||
	async put(@Param('id') id: number, @Body({ validate: true }) updateTrack: UpdateTrack) {
 | 
			
		||||
		let oldTrack = await this.trackRepository.findOne({ id: id });
 | 
			
		||||
 | 
			
		||||
		if (!oldTrack) {
 | 
			
		||||
			throw new TrackNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldTrack.id != track.id) {
 | 
			
		||||
		if (oldTrack.id != updateTrack.id) {
 | 
			
		||||
			throw new TrackIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
		await this.trackRepository.save(await updateTrack.update(oldTrack));
 | 
			
		||||
 | 
			
		||||
		await this.trackRepository.save(track);
 | 
			
		||||
		return new ResponseTrack(track);
 | 
			
		||||
		return new ResponseTrack(await this.trackRepository.findOne({ id: id }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
@@ -83,10 +86,19 @@ export class TrackController {
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: "Delete the track whose id you provided. <br> If no track with this id exists it will just return 204(no content)." })
 | 
			
		||||
	async remove(@Param("id") id: number) {
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let track = await this.trackRepository.findOne({ id: id });
 | 
			
		||||
		if (!track) { return null; }
 | 
			
		||||
 | 
			
		||||
		const trackStations = (await this.trackRepository.findOne({ id: id }, { relations: ["stations"] })).stations;
 | 
			
		||||
		if (trackStations.length != 0 && !force) {
 | 
			
		||||
			throw new TrackHasScanStationsError();
 | 
			
		||||
		}
 | 
			
		||||
		const stationController = new ScanStationController;
 | 
			
		||||
		for (let station of trackStations) {
 | 
			
		||||
			await stationController.remove(station.id, force);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.trackRepository.delete(track);
 | 
			
		||||
		return new ResponseTrack(track);
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { UserIdsNotMatchingError, UserNotFoundError } from '../errors/UserErrors';
 | 
			
		||||
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserDeletionNotConfirmedError, UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors';
 | 
			
		||||
import { UserGroupNotFoundError } from '../errors/UserGroupErrors';
 | 
			
		||||
import { CreateUser } from '../models/actions/CreateUser';
 | 
			
		||||
import { UpdateUser } from '../models/actions/UpdateUser';
 | 
			
		||||
import { CreateUser } from '../models/actions/create/CreateUser';
 | 
			
		||||
import { UpdateUser } from '../models/actions/update/UpdateUser';
 | 
			
		||||
import { User } from '../models/entities/User';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseUser } from '../models/responses/ResponseUser';
 | 
			
		||||
import { ResponseUserPermissions } from '../models/responses/ResponseUserPermissions';
 | 
			
		||||
import { PermissionController } from './PermissionController';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -25,11 +26,11 @@ export class UserController {
 | 
			
		||||
 | 
			
		||||
	@Get()
 | 
			
		||||
	@Authorized("USER:GET")
 | 
			
		||||
	@ResponseSchema(User, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all users. <br> This includes their groups and permissions directly granted to them (if existing/associated).' })
 | 
			
		||||
	@ResponseSchema(ResponseUser, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all users. <br> This includes their groups and permissions granted to them.' })
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseUsers: ResponseUser[] = new Array<ResponseUser>();
 | 
			
		||||
		const users = await this.userRepository.find({ relations: ['permissions', 'groups'] });
 | 
			
		||||
		const users = await this.userRepository.find({ relations: ['permissions', 'groups', 'groups.permissions'] });
 | 
			
		||||
		users.forEach(user => {
 | 
			
		||||
			responseUsers.push(new ResponseUser(user));
 | 
			
		||||
		});
 | 
			
		||||
@@ -38,38 +39,60 @@ export class UserController {
 | 
			
		||||
 | 
			
		||||
	@Get('/:id')
 | 
			
		||||
	@Authorized("USER:GET")
 | 
			
		||||
	@ResponseSchema(User)
 | 
			
		||||
	@ResponseSchema(ResponseUser)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(UserNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about the user whose id got provided. <br> Please remember that only permissions granted directly to the user will show up here, not permissions inherited from groups.' })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about the user whose id got provided. <br> Please remember that all permissions granted to the user will show up here.' })
 | 
			
		||||
	async getOne(@Param('id') id: number) {
 | 
			
		||||
		let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] })
 | 
			
		||||
		let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] })
 | 
			
		||||
		if (!user) { throw new UserNotFoundError(); }
 | 
			
		||||
		return new ResponseUser(user);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id/permissions')
 | 
			
		||||
	@Authorized("USER:GET")
 | 
			
		||||
	@ResponseSchema(ResponseUser)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(UserNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all permissions granted to the user sorted into directly granted and inherited as permission response objects.' })
 | 
			
		||||
	async getPermissions(@Param('id') id: number) {
 | 
			
		||||
		let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
 | 
			
		||||
		if (!user) { throw new UserNotFoundError(); }
 | 
			
		||||
		return new ResponseUserPermissions(user);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post()
 | 
			
		||||
	@Authorized("USER:CREATE")
 | 
			
		||||
	@ResponseSchema(User)
 | 
			
		||||
	@ResponseSchema(UserGroupNotFoundError)
 | 
			
		||||
	@ResponseSchema(ResponseUser)
 | 
			
		||||
	@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: 'Create a new user. <br> If you want to grant permissions to the user you have to create them seperately by posting to /api/permissions after creating the user.' })
 | 
			
		||||
	async post(@Body({ validate: true }) createUser: CreateUser) {
 | 
			
		||||
		let user;
 | 
			
		||||
		try {
 | 
			
		||||
			user = await createUser.toUser();
 | 
			
		||||
			user = await createUser.toEntity();
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		user = await this.userRepository.save(user)
 | 
			
		||||
		return new ResponseUser(await this.userRepository.findOne({ id: user.id }, { relations: ['permissions', 'groups'] }));
 | 
			
		||||
		return new ResponseUser(await this.userRepository.findOne({ id: user.id }, { relations: ['permissions', 'groups', 'groups.permissions'] }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/:id')
 | 
			
		||||
	@Authorized("USER:UPDATE")
 | 
			
		||||
	@ResponseSchema(User)
 | 
			
		||||
	@ResponseSchema(ResponseUser)
 | 
			
		||||
	@ResponseSchema(UserNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(PasswordMustContainUppercaseLetterError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(PasswordMustContainLowercaseLetterError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(PasswordMustContainNumberError, { statusCode: 406 })
 | 
			
		||||
	@ResponseSchema(PasswordTooShortError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Update the user whose id you provided. <br> To change the permissions directly granted to the user please use /api/permissions instead. <br> Please remember that ids can't be changed." })
 | 
			
		||||
	async put(@Param('id') id: number, @Body({ validate: true }) updateUser: UpdateUser) {
 | 
			
		||||
		let oldUser = await this.userRepository.findOne({ id: id });
 | 
			
		||||
@@ -81,21 +104,23 @@ export class UserController {
 | 
			
		||||
		if (oldUser.id != updateUser.id) {
 | 
			
		||||
			throw new UserIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
		await this.userRepository.save(await updateUser.updateUser(oldUser));
 | 
			
		||||
		await this.userRepository.save(await updateUser.update(oldUser));
 | 
			
		||||
 | 
			
		||||
		return new ResponseUser(await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] }));
 | 
			
		||||
		return new ResponseUser(await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
	@Authorized("USER:DELETE")
 | 
			
		||||
	@ResponseSchema(User)
 | 
			
		||||
	@ResponseSchema(ResponseUser)
 | 
			
		||||
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
 | 
			
		||||
	@ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 })
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: 'Delete the user whose id you provided. <br> If there are any permissions directly granted to the user they will get deleted as well. <br> If no user with this id exists it will just return 204(no content).' })
 | 
			
		||||
	@OpenAPI({ description: 'Delete the user whose id you provided. <br> You have to confirm your decision by providing the ?force=true query param. <br> If there are any permissions directly granted to the user they will get deleted as well. <br> If no user with this id exists it will just return 204(no content).' })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		if (!force) { throw new UserDeletionNotConfirmedError; }
 | 
			
		||||
		let user = await this.userRepository.findOne({ id: id });
 | 
			
		||||
		if (!user) { return null; }
 | 
			
		||||
		const responseUser = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] });;
 | 
			
		||||
		const responseUser = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] });;
 | 
			
		||||
 | 
			
		||||
		const permissionControler = new PermissionController();
 | 
			
		||||
		for (let permission of responseUser.permissions) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
 | 
			
		||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
 | 
			
		||||
import { getConnectionManager, Repository } from 'typeorm';
 | 
			
		||||
import { EntityFromBody } from 'typeorm-routing-controllers-extensions';
 | 
			
		||||
import { UserGroupIdsNotMatchingError, UserGroupNotFoundError } from '../errors/UserGroupErrors';
 | 
			
		||||
import { CreateUserGroup } from '../models/actions/CreateUserGroup';
 | 
			
		||||
import { CreateUserGroup } from '../models/actions/create/CreateUserGroup';
 | 
			
		||||
import { UpdateUserGroup } from '../models/actions/update/UpdateUserGroup';
 | 
			
		||||
import { UserGroup } from '../models/entities/UserGroup';
 | 
			
		||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
 | 
			
		||||
import { ResponseUserGroup } from '../models/responses/ResponseUserGroup';
 | 
			
		||||
import { ResponseUserGroupPermissions } from '../models/responses/ResponseUserGroupPermissions';
 | 
			
		||||
import { PermissionController } from './PermissionController';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -24,20 +25,37 @@ export class UserGroupController {
 | 
			
		||||
 | 
			
		||||
	@Get()
 | 
			
		||||
	@Authorized("USERGROUP:GET")
 | 
			
		||||
	@ResponseSchema(UserGroup, { isArray: true })
 | 
			
		||||
	@ResponseSchema(ResponseUserGroup, { isArray: true })
 | 
			
		||||
	@OpenAPI({ description: 'Lists all groups. <br> The information provided might change while the project continues to evolve.' })
 | 
			
		||||
	getAll() {
 | 
			
		||||
		return this.userGroupsRepository.find({ relations: ["permissions"] });
 | 
			
		||||
	async getAll() {
 | 
			
		||||
		let responseGroups: ResponseUserGroup[] = new Array<ResponseUserGroup>();
 | 
			
		||||
		const groups = await this.userGroupsRepository.find({ relations: ['permissions'] });
 | 
			
		||||
		groups.forEach(group => {
 | 
			
		||||
			responseGroups.push(group.toResponse());
 | 
			
		||||
		});
 | 
			
		||||
		return responseGroups;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id')
 | 
			
		||||
	@Authorized("USERGROUP:GET")
 | 
			
		||||
	@ResponseSchema(UserGroup)
 | 
			
		||||
	@ResponseSchema(ResponseUserGroup)
 | 
			
		||||
	@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(UserGroupNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all information about the group whose id got provided. <br> The information provided might change while the project continues to evolve.' })
 | 
			
		||||
	getOne(@Param('id') id: number) {
 | 
			
		||||
		return this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] });
 | 
			
		||||
	async getOne(@Param('id') id: number) {
 | 
			
		||||
		return await (await (this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] }))).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Get('/:id/permissions')
 | 
			
		||||
	@Authorized("USERGROUP:GET")
 | 
			
		||||
	@ResponseSchema(ResponseUserGroupPermissions)
 | 
			
		||||
	@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@OnUndefined(UserGroupNotFoundError)
 | 
			
		||||
	@OpenAPI({ description: 'Lists all permissions granted to the group as permission response objects.' })
 | 
			
		||||
	async getPermissions(@Param('id') id: number) {
 | 
			
		||||
		let group = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions', 'permissions.principal'] })
 | 
			
		||||
		if (!group) { throw new UserGroupNotFoundError(); }
 | 
			
		||||
		return new ResponseUserGroupPermissions(group);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Post()
 | 
			
		||||
@@ -48,12 +66,13 @@ export class UserGroupController {
 | 
			
		||||
	async post(@Body({ validate: true }) createUserGroup: CreateUserGroup) {
 | 
			
		||||
		let userGroup;
 | 
			
		||||
		try {
 | 
			
		||||
			userGroup = await createUserGroup.toUserGroup();
 | 
			
		||||
			userGroup = await createUserGroup.toEntity();
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return this.userGroupsRepository.save(userGroup);
 | 
			
		||||
		userGroup = await this.userGroupsRepository.save(userGroup);
 | 
			
		||||
		return (await (this.userGroupsRepository.findOne({ id: userGroup.id }, { relations: ["permissions"] }))).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Put('/:id')
 | 
			
		||||
@@ -62,19 +81,19 @@ export class UserGroupController {
 | 
			
		||||
	@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
 | 
			
		||||
	@ResponseSchema(UserGroupIdsNotMatchingError, { statusCode: 406 })
 | 
			
		||||
	@OpenAPI({ description: "Update the group whose id you provided. <br> To change the permissions granted to the group please use /api/permissions instead. <br> Please remember that ids can't be changed." })
 | 
			
		||||
	async put(@Param('id') id: number, @EntityFromBody() userGroup: UserGroup) {
 | 
			
		||||
		let oldUserGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] });
 | 
			
		||||
	async put(@Param('id') id: number, @Body({ validate: true }) updateGroup: UpdateUserGroup) {
 | 
			
		||||
		let oldGroup = await this.userGroupsRepository.findOne({ id: id });
 | 
			
		||||
 | 
			
		||||
		if (!oldUserGroup) {
 | 
			
		||||
			throw new UserGroupNotFoundError()
 | 
			
		||||
		if (!oldGroup) {
 | 
			
		||||
			throw new UserGroupNotFoundError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (oldUserGroup.id != userGroup.id) {
 | 
			
		||||
		if (oldGroup.id != updateGroup.id) {
 | 
			
		||||
			throw new UserGroupIdsNotMatchingError();
 | 
			
		||||
		}
 | 
			
		||||
		await this.userGroupsRepository.save(await updateGroup.update(oldGroup));
 | 
			
		||||
 | 
			
		||||
		await this.userGroupsRepository.save(userGroup);
 | 
			
		||||
		return userGroup;
 | 
			
		||||
		return (await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] })).toResponse();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Delete('/:id')
 | 
			
		||||
@@ -84,13 +103,13 @@ export class UserGroupController {
 | 
			
		||||
	@OnUndefined(204)
 | 
			
		||||
	@OpenAPI({ description: 'Delete the group whose id you provided. <br> If there are any permissions directly granted to the group they will get deleted as well. <br> Users associated with this group won\'t get deleted - just deassociated. <br> If no group with this id exists it will just return 204(no content).' })
 | 
			
		||||
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
 | 
			
		||||
		let group = await this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] });
 | 
			
		||||
		let group = await this.userGroupsRepository.findOne({ id: id });
 | 
			
		||||
		if (!group) { return null; }
 | 
			
		||||
		const responseGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] });
 | 
			
		||||
 | 
			
		||||
		const permissionControler = new PermissionController();
 | 
			
		||||
		const permissionController = new PermissionController();
 | 
			
		||||
		for (let permission of responseGroup.permissions) {
 | 
			
		||||
			await permissionControler.remove(permission.id, true);
 | 
			
		||||
			await permissionController.remove(permission.id, true);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.userGroupsRepository.delete(group);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,57 @@
 | 
			
		||||
import { IsString } from 'class-validator';
 | 
			
		||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
 | 
			
		||||
import { BadRequestError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw, when to provided address doesn't belong to the accepted types.
 | 
			
		||||
 * Error to throw when an address's postal code fails validation.
 | 
			
		||||
 */
 | 
			
		||||
export class AddressWrongTypeError extends NotAcceptableError {
 | 
			
		||||
export class AddressPostalCodeInvalidError extends BadRequestError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "AddressWrongTypeError"
 | 
			
		||||
	name = "AddressPostalCodeInvalidError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The address must be an existing adress's id. \n You provided a object of another type."
 | 
			
		||||
	message = "The postal code you provided is invalid. \n Please check if your postal code follows the postal code validation guidelines."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw, when a non-existant address get's loaded.
 | 
			
		||||
 * Error to throw when an non-empty address's first line isn't set.
 | 
			
		||||
 */
 | 
			
		||||
export class AddressNotFoundError extends NotFoundError {
 | 
			
		||||
export class AddressFirstLineEmptyError extends BadRequestError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "AddressNotFoundError"
 | 
			
		||||
	name = "AddressFirstLineEmptyError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The address you provided couldn't be located in the system. \n Please check your request."
 | 
			
		||||
	message = "You provided a empty first address line. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when an non-empty address's postal code isn't set.
 | 
			
		||||
 */
 | 
			
		||||
export class AddressPostalCodeEmptyError extends BadRequestError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "AddressPostalCodeEmptyError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "You provided a empty postal code. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when an non-empty address's city isn't set.
 | 
			
		||||
 */
 | 
			
		||||
export class AddressCityEmptyError extends BadRequestError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "AddressCityEmptyError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "You provided a empty city. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when an non-empty address's country isn't set.
 | 
			
		||||
 */
 | 
			
		||||
export class AddressCountryEmptyError extends BadRequestError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "AddressCountryEmptyError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "You provided a empty country. \n If you want an empty address please set all propertys to null. \n For non-empty addresses the following fields have to be set: address1, postalcode, city, country"
 | 
			
		||||
}
 | 
			
		||||
@@ -118,7 +118,7 @@ export class RefreshTokenCountInvalidError extends NotAcceptableError {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when someone tryes to reset a user's password more than once in 15 minutes.
 | 
			
		||||
 * Error to throw when someone tries to reset a user's password more than once in 15 minutes.
 | 
			
		||||
 */
 | 
			
		||||
export class ResetAlreadyRequestedError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								src/errors/DonationErrors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/errors/DonationErrors.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
import { IsString } from 'class-validator';
 | 
			
		||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a Donation couldn't be found.
 | 
			
		||||
 */
 | 
			
		||||
export class DonationNotFoundError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "DonationNotFoundError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "Donation not found!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when two Donations' ids don't match.
 | 
			
		||||
 * Usually occurs when a user tries to change a Donation's id.
 | 
			
		||||
 */
 | 
			
		||||
export class DonationIdsNotMatchingError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "DonationIdsNotMatchingError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The ids don't match! \n And if you wanted to change a Donation's id: This isn't allowed!"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								src/errors/DonorErrors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/errors/DonorErrors.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
import { IsString } from 'class-validator';
 | 
			
		||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a donor couldn't be found.
 | 
			
		||||
 */
 | 
			
		||||
export class DonorNotFoundError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "DonorNotFoundError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "Donor not found!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when two donors' ids don't match.
 | 
			
		||||
 * Usually occurs when a user tries to change a donor's id.
 | 
			
		||||
 */
 | 
			
		||||
export class DonorIdsNotMatchingError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "DonorIdsNotMatchingError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The ids don't match! \n And if you wanted to change a donor's id: This isn't allowed!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a donor needs a receipt, but no address is associated with them.
 | 
			
		||||
 */
 | 
			
		||||
export class DonorReceiptAddressNeededError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "DonorReceiptAddressNeededError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "An address is needed to create a receipt for a donor. \n You didn't provide one."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
* Error to throw when a donor still has donations associated.
 | 
			
		||||
*/
 | 
			
		||||
export class DonorHasDonationsError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "DonorHasDonationsError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "This donor still has donations associated with it. \n If you want to delete this donor with all it's donations and teams add `?force` to your query."
 | 
			
		||||
}
 | 
			
		||||
@@ -2,18 +2,7 @@ import { IsString } from 'class-validator';
 | 
			
		||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw, when a provided groupContact doesn't belong to the accepted types.
 | 
			
		||||
 */
 | 
			
		||||
export class GroupContactWrongTypeError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "GroupContactWrongTypeError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The groupContact must be an existing groupContact's id. \n You provided a object of another type."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw, when a non-existant groupContact get's loaded.
 | 
			
		||||
 * Error to throw, when a non-existent contact get's requested.
 | 
			
		||||
 */
 | 
			
		||||
export class GroupContactNotFoundError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
@@ -21,4 +10,16 @@ export class GroupContactNotFoundError extends NotFoundError {
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The groupContact you provided couldn't be located in the system. \n Please check your request."
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when two contacts' ids don't match.
 | 
			
		||||
 * Usually occurs when a user tries to change a contact's id.
 | 
			
		||||
 */
 | 
			
		||||
export class GroupContactIdsNotMatchingError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "GroupContactIdsNotMatchingError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The ids don't match! \n And if you wanted to change a contact's id: This isn't allowed!"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								src/errors/MailErrors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/errors/MailErrors.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
import { IsString } from 'class-validator';
 | 
			
		||||
import { InternalServerError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a permission couldn't be found.
 | 
			
		||||
 */
 | 
			
		||||
export class MailSendingError extends InternalServerError {
 | 
			
		||||
    @IsString()
 | 
			
		||||
    name = "MailSendingError"
 | 
			
		||||
 | 
			
		||||
    @IsString()
 | 
			
		||||
    message = "We had a problem sending the mail!"
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super("We had a problem sending the mail!");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,12 +13,12 @@ export class PrincipalNotFoundError extends NotFoundError {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw, when a provided runnerOrganisation doesn't belong to the accepted types.
 | 
			
		||||
 * Error to throw, when a provided runner organization doesn't belong to the accepted types.
 | 
			
		||||
 */
 | 
			
		||||
export class PrincipalWrongTypeError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "PrincipalWrongTypeError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The princial must have an existing principal's id. \n You provided a object of another type."
 | 
			
		||||
	message = "The principal must have an existing principal's id. \n You provided a object of another type."
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										48
									
								
								src/errors/RunnerCardErrors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/errors/RunnerCardErrors.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
import { IsString } from 'class-validator';
 | 
			
		||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a card couldn't be found.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerCardNotFoundError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerCardNotFoundError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "Card not found!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when two cards' ids don't match.
 | 
			
		||||
 * Usually occurs when a user tries to change a card's id.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerCardIdsNotMatchingError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerCardIdsNotMatchingError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The ids don't match! \n And if you wanted to change a cards's id: This isn't allowed"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a card still has scans associated.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerCardHasScansError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerCardHasScansError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "This card still has scans associated with it. \n If you want to delete this card with all it's scans add `?force` to your query. \n Otherwise please consider just disabling it."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a card's id is too big to generate a ean-13 barcode for it.
 | 
			
		||||
 * This error should never reach a end user.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerCardIdOutOfRangeError extends Error {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerCardIdOutOfRangeError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The card's id is too big to fit into a ean-13 barcode. \n This has a very low probability of happening but means that you might want to switch your barcode format for something that can accept numbers over 9999999999."
 | 
			
		||||
}
 | 
			
		||||
@@ -32,5 +32,38 @@ export class RunnerGroupNeededError extends NotAcceptableError {
 | 
			
		||||
	name = "RunnerGroupNeededError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "Runner's need to be part of one group (team or organisiation)! \n You provided neither."
 | 
			
		||||
	message = "Runner's need to be part of one group (team or organization)! \n You provided neither."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a citizen runner has no mail-address.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerEmailNeededError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerEmailNeededError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "Citizenrunners have to provide an email address for verification and contacting."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a runner already requested a new selfservice link in the last 24hrs.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerSelfserviceTimeoutError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerSelfserviceTimeoutError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "You can only reqest a new token every 24hrs."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
* Error to throw when a runner still has distance donations associated.
 | 
			
		||||
*/
 | 
			
		||||
export class RunnerHasDistanceDonationsError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerHasDistanceDonationsError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "This runner still has distance donations associated with it. \n If you want to delete this runner with all it's donations and teams add `?force` to your query."
 | 
			
		||||
}
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
import { IsString } from 'class-validator';
 | 
			
		||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a runner organisation couldn't be found.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerOrganisationNotFoundError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerOrganisationNotFoundError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "RunnerOrganisation not found!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when two runner organisations' ids don't match.
 | 
			
		||||
 * Usually occurs when a user tries to change a runner organisation's id.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerOrganisationIdsNotMatchingError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerOrganisationIdsNotMatchingError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The ids don't match! \n And if you wanted to change a runner organisation's id: This isn't allowed!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a organisation still has runners associated.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerOrganisationHasRunnersError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerOrganisationHasRunnersError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "This organisation still has runners associated with it. \n If you want to delete this organisation with all it's runners and teams add `?force` to your query."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a organisation still has teams associated.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerOrganisationHasTeamsError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerOrganisationHasTeamsError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "This organisation still has teams associated with it. \n If you want to delete this organisation with all it's runners and teams add `?force` to your query."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw, when a provided runnerOrganisation doesn't belong to the accepted types.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerOrganisationWrongTypeError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerOrganisationWrongTypeError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The runner organisation must be an existing organisation's id. \n You provided a object of another type."
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										58
									
								
								src/errors/RunnerOrganizationErrors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/errors/RunnerOrganizationErrors.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
import { IsString } from 'class-validator';
 | 
			
		||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a runner organization couldn't be found.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerOrganizationNotFoundError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerOrganizationNotFoundError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "RunnerOrganization not found!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when two runner organization's ids don't match.
 | 
			
		||||
 * Usually occurs when a user tries to change a runner organization's id.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerOrganizationIdsNotMatchingError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerOrganizationIdsNotMatchingError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The ids don't match! \n And if you wanted to change a runner organization's id: This isn't allowed!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a organization still has runners associated.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerOrganizationHasRunnersError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerOrganizationHasRunnersError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "This organization still has runners associated with it. \n If you want to delete this organization with all it's runners and teams add `?force` to your query."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a organization still has teams associated.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerOrganizationHasTeamsError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerOrganizationHasTeamsError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "This organization still has teams associated with it. \n If you want to delete this organization with all it's runners and teams add `?force` to your query."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw, when a provided runnerOrganization doesn't belong to the accepted types.
 | 
			
		||||
 */
 | 
			
		||||
export class RunnerOrganizationWrongTypeError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "RunnerOrganizationWrongTypeError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The runner organization must be an existing organization's id. \n You provided a object of another type."
 | 
			
		||||
}
 | 
			
		||||
@@ -43,5 +43,5 @@ export class RunnerTeamNeedsParentError extends NotAcceptableError {
 | 
			
		||||
	name = "RunnerTeamNeedsParentError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "You provided no runner organisation as this team's parent group."
 | 
			
		||||
	message = "You provided no runner organization as this team's parent group."
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								src/errors/ScanErrors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/errors/ScanErrors.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
import { IsString } from 'class-validator';
 | 
			
		||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a Scan couldn't be found.
 | 
			
		||||
 */
 | 
			
		||||
export class ScanNotFoundError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "ScanNotFoundError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "Scan not found!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when two Scans' ids don't match.
 | 
			
		||||
 * Usually occurs when a user tries to change a Scan's id.
 | 
			
		||||
 */
 | 
			
		||||
export class ScanIdsNotMatchingError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "ScanIdsNotMatchingError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The ids don't match! \n And if you wanted to change a Scan's id: This isn't allowed!"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								src/errors/ScanStationErrors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/errors/ScanStationErrors.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
import { IsString } from 'class-validator';
 | 
			
		||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw, when a non-existent scan station get's loaded.
 | 
			
		||||
 */
 | 
			
		||||
export class ScanStationNotFoundError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "ScanStationNotFoundError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The scan station you provided couldn't be located in the system. \n Please check your request."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when two scan stations' ids don't match.
 | 
			
		||||
 * Usually occurs when a user tries to change a scan station's id.
 | 
			
		||||
 */
 | 
			
		||||
export class ScanStationIdsNotMatchingError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "ScanStationIdsNotMatchingError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The ids don't match! \n And if you wanted to change a scan station's id: This isn't allowed!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a station still has scans associated.
 | 
			
		||||
 */
 | 
			
		||||
export class ScanStationHasScansError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "ScanStationHasScansError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "This station still has scans associated with it. \n If you want to delete this station with all it's scans add `?force` to your query."
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								src/errors/StatsClientErrors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/errors/StatsClientErrors.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
import { IsString } from 'class-validator';
 | 
			
		||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw, when a non-existent stats client get's loaded.
 | 
			
		||||
 */
 | 
			
		||||
export class StatsClientNotFoundError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "StatsClientNotFoundError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The stats client you provided couldn't be located in the system. \n Please check your request."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when two stats clients' ids don't match.
 | 
			
		||||
 * Usually occurs when a user tries to change a stats client's id.
 | 
			
		||||
 */
 | 
			
		||||
export class StatsClientIdsNotMatchingError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "StatsClientIdsNotMatchingError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The ids don't match! \n And if you wanted to change a stats client's id: This isn't allowed!"
 | 
			
		||||
}
 | 
			
		||||
@@ -22,4 +22,23 @@ export class TrackIdsNotMatchingError extends NotAcceptableError {
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The ids don't match! \n And if you wanted to change a track's id: This isn't allowed"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a track's lap time is set to a negative value.
 | 
			
		||||
 */
 | 
			
		||||
export class TrackLapTimeCantBeNegativeError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "TrackLapTimeCantBeNegativeError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The minimum lap time you provided is negative - That isn't possible. \n If you wanted to disable it: Just set it to 0/null."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class TrackHasScanStationsError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "TrackHasScanStationsError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "This track still has stations associated with it. \n If you want to delete this track with all it's stations and scans add `?force` to your query."
 | 
			
		||||
}
 | 
			
		||||
@@ -4,7 +4,7 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when no username or email is set.
 | 
			
		||||
 * We somehow need to identify you :)
 | 
			
		||||
 * We somehow need to identify you on login.
 | 
			
		||||
 */
 | 
			
		||||
export class UsernameOrEmailNeededError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
@@ -14,6 +14,30 @@ export class UsernameOrEmailNeededError extends NotFoundError {
 | 
			
		||||
	message = "No username or email is set!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when no username contains illegal characters.
 | 
			
		||||
 * Right now the only one is "@" but this could change in the future.
 | 
			
		||||
 */
 | 
			
		||||
export class UsernameContainsIllegalCharacterError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "UsernameContainsIllegalCharacterError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The provided username contains illegal characters! \n Right now the following characters are considered illegal: '@'"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when no email is set.
 | 
			
		||||
 * We somehow need to identify you :)
 | 
			
		||||
 */
 | 
			
		||||
export class UserEmailNeededError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "UserEmailNeededError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "No email is set! \n You have to provide email addresses for users (used for password reset among others)."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a user couldn't be found.
 | 
			
		||||
 */
 | 
			
		||||
@@ -35,4 +59,45 @@ export class UserIdsNotMatchingError extends NotAcceptableError {
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The ids don't match!! \n And if you wanted to change a user's id: This isn't allowed!"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when two users' ids don't match.
 | 
			
		||||
 * Usually occurs when a user tries to change a user's id.
 | 
			
		||||
 */
 | 
			
		||||
export class UserDeletionNotConfirmedError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "UserDeletionNotConfirmedError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "You are trying to delete a user! \n If you're sure about doing this: provide the ?force=true query param."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class PasswordMustContainUppercaseLetterError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "PasswordMustContainUppercaseLetterError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "Passwords must contain at least one uppercase letter."
 | 
			
		||||
}
 | 
			
		||||
export class PasswordMustContainLowercaseLetterError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "PasswordMustContainLowercaseLetterError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "Passwords must contain at least one lowercase letter."
 | 
			
		||||
}
 | 
			
		||||
export class PasswordMustContainNumberError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "PasswordMustContainNumberError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "Passwords must contain at least one number."
 | 
			
		||||
}
 | 
			
		||||
export class PasswordTooShortError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "PasswordTooShortError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "Passwords must be at least ten characters long."
 | 
			
		||||
}
 | 
			
		||||
@@ -2,7 +2,7 @@ import { IsString } from 'class-validator';
 | 
			
		||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when no groupname is set.
 | 
			
		||||
 * Error to throw when no group name is set.
 | 
			
		||||
 */
 | 
			
		||||
export class GroupNameNeededError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
@@ -13,7 +13,7 @@ export class GroupNameNeededError extends NotFoundError {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when a usergroup couldn't be found.
 | 
			
		||||
 * Error to throw when a user group couldn't be found.
 | 
			
		||||
 */
 | 
			
		||||
export class UserGroupNotFoundError extends NotFoundError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
@@ -24,13 +24,13 @@ export class UserGroupNotFoundError extends NotFoundError {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Error to throw when two usergroups' ids don't match.
 | 
			
		||||
 * Usually occurs when a user tries to change a usergroups's id.
 | 
			
		||||
 * Error to throw when two user groups' ids don't match.
 | 
			
		||||
 * Usually occurs when a user tries to change a user groups's id.
 | 
			
		||||
 */
 | 
			
		||||
export class UserGroupIdsNotMatchingError extends NotAcceptableError {
 | 
			
		||||
	@IsString()
 | 
			
		||||
	name = "UserGroupIdsNotMatchingError"
 | 
			
		||||
 | 
			
		||||
	@IsString()
 | 
			
		||||
	message = "The ids don't match!! \n If you wanted to change a usergroup's id: This isn't allowed!"
 | 
			
		||||
	message = "The ids don't match!! \n If you wanted to change a user group's id: This isn't allowed!"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
 | 
			
		||||
import * as jsonwebtoken from "jsonwebtoken";
 | 
			
		||||
import { config } from './config';
 | 
			
		||||
import { Runner } from './models/entities/Runner';
 | 
			
		||||
import { User } from './models/entities/User';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -34,6 +35,19 @@ export class JwtCreator {
 | 
			
		||||
        }, config.jwt_secret)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new selfservice token for a given runner.
 | 
			
		||||
     * @param runner Runner entity that the access token shall be created for.
 | 
			
		||||
     * @param expiry_timestamp Timestamp for the token expiry. Will be set about 9999 years if none provided.
 | 
			
		||||
     */
 | 
			
		||||
    public static createSelfService(runner: Runner, expiry_timestamp?: number) {
 | 
			
		||||
        if (!expiry_timestamp) { expiry_timestamp = Math.floor(Date.now() / 1000) + 36000 * 60 * 24 * 365 * 9999; }
 | 
			
		||||
        return jsonwebtoken.sign({
 | 
			
		||||
            id: runner.id,
 | 
			
		||||
            exp: expiry_timestamp
 | 
			
		||||
        }, config.jwt_secret)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new password reset token for a given user.
 | 
			
		||||
     * The token is valid for 15 minutes or 1 use - whatever comes first.
 | 
			
		||||
@@ -106,23 +120,6 @@ export class JwtUser {
 | 
			
		||||
        this.refreshTokenCount = user.refreshTokenCount;
 | 
			
		||||
        this.uuid = user.uuid;
 | 
			
		||||
        this.profilePic = user.profilePic;
 | 
			
		||||
        this.permissions = this.getPermissions(user);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handels getting the permissions granted to this user (direct or indirect).
 | 
			
		||||
     * @param user User which's permissions shall be gotten.
 | 
			
		||||
     */
 | 
			
		||||
    public getPermissions(user: User): string[] {
 | 
			
		||||
        let returnPermissions: string[] = new Array<string>();
 | 
			
		||||
        for (let permission of user.permissions) {
 | 
			
		||||
            returnPermissions.push(permission.toString());
 | 
			
		||||
        }
 | 
			
		||||
        for (let group of user.groups) {
 | 
			
		||||
            for (let permission of group.permissions) {
 | 
			
		||||
                returnPermissions.push(permission.toString());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return Array.from(new Set(returnPermissions));
 | 
			
		||||
        this.permissions = user.allPermissions;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,9 @@
 | 
			
		||||
import { createConnection } from "typeorm";
 | 
			
		||||
import { runSeeder } from 'typeorm-seeding';
 | 
			
		||||
import { User } from '../models/entities/User';
 | 
			
		||||
import { config } from '../config';
 | 
			
		||||
import { ConfigFlag } from '../models/entities/ConfigFlags';
 | 
			
		||||
import SeedPublicOrg from '../seeds/SeedPublicOrg';
 | 
			
		||||
import SeedTestRunners from '../seeds/SeedTestRunners';
 | 
			
		||||
import SeedUsers from '../seeds/SeedUsers';
 | 
			
		||||
/**
 | 
			
		||||
 * Loader for the database that creates the database connection and initializes the database tabels.
 | 
			
		||||
@@ -9,8 +12,20 @@ import SeedUsers from '../seeds/SeedUsers';
 | 
			
		||||
export default async () => {
 | 
			
		||||
    const connection = await createConnection();
 | 
			
		||||
    await connection.synchronize();
 | 
			
		||||
    if (await connection.getRepository(User).count() === 0) {
 | 
			
		||||
 | 
			
		||||
    //The data seeding part
 | 
			
		||||
    if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:user", value: "true" }))) {
 | 
			
		||||
        await runSeeder(SeedUsers);
 | 
			
		||||
        await connection.getRepository(ConfigFlag).save({ option: "seeded:user", value: "true" });
 | 
			
		||||
    }
 | 
			
		||||
    if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:citizenorg", value: "true" }))) {
 | 
			
		||||
        await runSeeder(SeedPublicOrg);
 | 
			
		||||
        await connection.getRepository(ConfigFlag).save({ option: "seeded:citizenorg", value: "true" });
 | 
			
		||||
    }
 | 
			
		||||
    if (!(await connection.getRepository(ConfigFlag).findOne({ option: "seeded:testdata", value: "true" })) && config.seedTestData == true) {
 | 
			
		||||
        await runSeeder(SeedTestRunners);
 | 
			
		||||
        await connection.getRepository(ConfigFlag).save({ option: "seeded:testdata", value: "true" });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return connection;
 | 
			
		||||
};
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import { validationMetadatasToSchemas } from "class-validator-jsonschema";
 | 
			
		||||
import { validationMetadatasToSchemas } from "@odit/class-validator-jsonschema";
 | 
			
		||||
import express, { Application } from "express";
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import { getMetadataArgsStorage } from "routing-controllers";
 | 
			
		||||
import { routingControllersToSpec } from "routing-controllers-openapi";
 | 
			
		||||
import { generateSpec } from '../apispec';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Loader for everything openapi related - from creating the schema to serving it via a static route and swaggerUiExpress.
 | 
			
		||||
@@ -15,36 +15,7 @@ export default async (app: Application) => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  //Spec creation based on the previously created schemas
 | 
			
		||||
  const spec = routingControllersToSpec(
 | 
			
		||||
    storage,
 | 
			
		||||
    {
 | 
			
		||||
      routePrefix: "/api"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      components: {
 | 
			
		||||
        schemas,
 | 
			
		||||
        "securitySchemes": {
 | 
			
		||||
          "AuthToken": {
 | 
			
		||||
            "type": "http",
 | 
			
		||||
            "scheme": "bearer",
 | 
			
		||||
            "bearerFormat": "JWT",
 | 
			
		||||
            description: "A JWT based access token. Use /api/auth/login or /api/auth/refresh to get one."
 | 
			
		||||
          },
 | 
			
		||||
          "RefreshTokenCookie": {
 | 
			
		||||
            "type": "apiKey",
 | 
			
		||||
            "in": "cookie",
 | 
			
		||||
            "name": "lfk_backend__refresh_token",
 | 
			
		||||
            description: "A cookie containing a JWT based refreh token. Attention: Doesn't work in swagger-ui. Use /api/auth/login or /api/auth/refresh to get one."
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      info: {
 | 
			
		||||
        description: "The the backend API for the LfK! runner system.",
 | 
			
		||||
        title: "LfK! Backend API",
 | 
			
		||||
        version: "0.0.5",
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
  const spec = generateSpec(storage, schemas);
 | 
			
		||||
  app.get(["/api/docs/openapi.json", "/api/docs/swagger.json"], (req, res) => {
 | 
			
		||||
    res.json(spec);
 | 
			
		||||
  });
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										64
									
								
								src/mailer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/mailer.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import { config } from './config';
 | 
			
		||||
import { MailSendingError } from './errors/MailErrors';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is responsible for all things mail sending.
 | 
			
		||||
 * This uses axios to communicate with the mailer api (https://git.odit.services/lfk/mailer).
 | 
			
		||||
 */
 | 
			
		||||
export class Mailer {
 | 
			
		||||
    public static base: string = config.mailer_url;
 | 
			
		||||
    public static key: string = config.mailer_key;
 | 
			
		||||
    public static testing: boolean = config.testing;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function for sending a password reset mail.
 | 
			
		||||
     * @param to_address The address the mail will be sent to. Should always get pulled from a user object.
 | 
			
		||||
     * @param token The requested password reset token - will be combined with the app_url to generate a password reset link.
 | 
			
		||||
     */
 | 
			
		||||
    public static async sendResetMail(to_address: string, token: string, locale: string = "en") {
 | 
			
		||||
        try {
 | 
			
		||||
            await axios.post(`${Mailer.base}/reset?locale=${locale}&key=${Mailer.key}`, {
 | 
			
		||||
                address: to_address,
 | 
			
		||||
                resetKey: token
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (Mailer.testing) { return true; }
 | 
			
		||||
            throw new MailSendingError();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function for sending a runner selfservice welcome mail.
 | 
			
		||||
     * @param to_address The address the mail will be sent to. Should always get pulled from a runner object.
 | 
			
		||||
     * @param token The requested selfservice token - will be combined with the app_url to generate a selfservice profile link.
 | 
			
		||||
     */
 | 
			
		||||
    public static async sendSelfserviceWelcomeMail(to_address: string, token: string, locale: string = "en") {
 | 
			
		||||
        try {
 | 
			
		||||
            await axios.post(`${Mailer.base}/registration?locale=${locale}&key=${Mailer.key}`, {
 | 
			
		||||
                address: to_address,
 | 
			
		||||
                selfserviceToken: token
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (Mailer.testing) { return true; }
 | 
			
		||||
            throw new MailSendingError();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function for sending a runner selfservice link forgotten mail.
 | 
			
		||||
     * @param to_address The address the mail will be sent to. Should always get pulled from a runner object.
 | 
			
		||||
     * @param token The requested selfservice token - will be combined with the app_url to generate a selfservice profile link.
 | 
			
		||||
     */
 | 
			
		||||
    public static async sendSelfserviceForgottenMail(to_address: string, token: string, locale: string = "en") {
 | 
			
		||||
        try {
 | 
			
		||||
            await axios.post(`${Mailer.base}/registration_forgot?locale=${locale}&key=${Mailer.key}`, {
 | 
			
		||||
                address: to_address,
 | 
			
		||||
                selfserviceToken: token
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (Mailer.testing) { return true; }
 | 
			
		||||
            throw new MailSendingError();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import { Request, Response } from 'express';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Custom express middleware that appends the raw body to the request obeject.
 | 
			
		||||
 * Mainly used for parsing csvs from boddies.
 | 
			
		||||
 * Custom express middleware that appends the raw body to the request object.
 | 
			
		||||
 * Mainly used for parsing csvs from bodies.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const RawBodyMiddleware = (req: Request, res: Response, next: () => void) => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										69
									
								
								src/middlewares/ScanAuth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/middlewares/ScanAuth.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
			
		||||
import * as argon2 from "argon2";
 | 
			
		||||
import { Request, Response } from 'express';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { ScanStation } from '../models/entities/ScanStation';
 | 
			
		||||
import authchecker from './authchecker';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This middleware handles the authentication of scan station api tokens.
 | 
			
		||||
 * The tokens have to be provided via Bearer authorization header.
 | 
			
		||||
 * You have to manually use this middleware via @UseBefore(ScanAuth) instead of using @Authorized().
 | 
			
		||||
 * @param req Express request object.
 | 
			
		||||
 * @param res Express response object.
 | 
			
		||||
 * @param next Next function to call on success.
 | 
			
		||||
 */
 | 
			
		||||
const ScanAuth = async (req: Request, res: Response, next: () => void) => {
 | 
			
		||||
    let provided_token: string = req.headers["authorization"];
 | 
			
		||||
    if (provided_token == "" || provided_token === undefined || provided_token === null) {
 | 
			
		||||
        res.status(401).send({ http_code: 401, short: "no_token", message: "No api token provided." });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        provided_token = provided_token.replace("Bearer ", "");
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        res.status(401).send({ http_code: 401, short: "no_token", message: "No valid jwt or api token provided." });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let prefix = "";
 | 
			
		||||
    try {
 | 
			
		||||
        prefix = provided_token.split(".")[0];
 | 
			
		||||
    }
 | 
			
		||||
    finally {
 | 
			
		||||
        if (prefix == "" || prefix == undefined || prefix == null) {
 | 
			
		||||
            res.status(401).send({ http_code: 401, short: "invalid_token", message: "Api token non-existent or invalid syntax." });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const station = await getConnectionManager().get().getRepository(ScanStation).findOne({ prefix: prefix });
 | 
			
		||||
    if (!station) {
 | 
			
		||||
        let user_authorized = false;
 | 
			
		||||
        try {
 | 
			
		||||
            let action = { request: req, response: res, context: null, next: next }
 | 
			
		||||
            user_authorized = await authchecker(action, ["SCAN:CREATE"]);
 | 
			
		||||
        }
 | 
			
		||||
        finally {
 | 
			
		||||
            if (user_authorized == false) {
 | 
			
		||||
                res.status(401).send({ http_code: 401, short: "invalid_token", message: "Api token non-existent or invalid syntax." });
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                next();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        if (station.enabled == false) {
 | 
			
		||||
            res.status(401).send({ http_code: 401, short: "station_disabled", message: "Station is disabled." });
 | 
			
		||||
        }
 | 
			
		||||
        if (!(await argon2.verify(station.key, provided_token))) {
 | 
			
		||||
            res.status(401).send({ http_code: 401, short: "invalid_token", message: "Api token non-existent or invalid syntax." });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        req.headers["station_id"] = station.id.toString();
 | 
			
		||||
        next();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
export default ScanAuth;
 | 
			
		||||
							
								
								
									
										66
									
								
								src/middlewares/StatsAuth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/middlewares/StatsAuth.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
import * as argon2 from "argon2";
 | 
			
		||||
import { Request, Response } from 'express';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { StatsClient } from '../models/entities/StatsClient';
 | 
			
		||||
import authchecker from './authchecker';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This middleware handles the authentication of stats client api tokens.
 | 
			
		||||
 * The tokens have to be provided via Bearer authorization header.
 | 
			
		||||
 * You have to manually use this middleware via @UseBefore(StatsAuth) instead of using @Authorized().
 | 
			
		||||
 * @param req Express request object.
 | 
			
		||||
 * @param res Express response object.
 | 
			
		||||
 * @param next Next function to call on success.
 | 
			
		||||
 */
 | 
			
		||||
const StatsAuth = async (req: Request, res: Response, next: () => void) => {
 | 
			
		||||
    let provided_token: string = req.headers["authorization"];
 | 
			
		||||
    if (provided_token == "" || provided_token === undefined || provided_token === null) {
 | 
			
		||||
        res.status(401).send("No api token provided.");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        provided_token = provided_token.replace("Bearer ", "");
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        res.status(401).send("No valid jwt or api token provided.");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let prefix = "";
 | 
			
		||||
    try {
 | 
			
		||||
        prefix = provided_token.split(".")[0];
 | 
			
		||||
    }
 | 
			
		||||
    finally {
 | 
			
		||||
        if (prefix == "" || prefix == undefined || prefix == null) {
 | 
			
		||||
            res.status(401).send("Api token non-existant or invalid syntax.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const client = await getConnectionManager().get().getRepository(StatsClient).findOne({ prefix: prefix });
 | 
			
		||||
    if (!client) {
 | 
			
		||||
        let user_authorized = false;
 | 
			
		||||
        try {
 | 
			
		||||
            let action = { request: req, response: res, context: null, next: next }
 | 
			
		||||
            user_authorized = await authchecker(action, ["RUNNER:GET", "TEAM:GET", "ORGANIZATION:GET"]);
 | 
			
		||||
        }
 | 
			
		||||
        finally {
 | 
			
		||||
            if (user_authorized == false) {
 | 
			
		||||
                res.status(401).send("Api token non-existant or invalid syntax.");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                next();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        if (!(await argon2.verify(client.key, provided_token))) {
 | 
			
		||||
            res.status(401).send("Api token invalid.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        next();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
export default StatsAuth;
 | 
			
		||||
							
								
								
									
										58
									
								
								src/middlewares/UserChecker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/middlewares/UserChecker.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
import cookie from "cookie";
 | 
			
		||||
import * as jwt from "jsonwebtoken";
 | 
			
		||||
import { Action } from 'routing-controllers';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { config } from '../config';
 | 
			
		||||
import { IllegalJWTError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from '../errors/AuthError';
 | 
			
		||||
import { JwtCreator, JwtUser } from '../jwtcreator';
 | 
			
		||||
import { User } from '../models/entities/User';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODO:
 | 
			
		||||
 */
 | 
			
		||||
const UserChecker = async (action: Action) => {
 | 
			
		||||
    let jwtPayload = undefined
 | 
			
		||||
    try {
 | 
			
		||||
        let provided_token = "" + action.request.headers["authorization"].replace("Bearer ", "");
 | 
			
		||||
        jwtPayload = <any>jwt.verify(provided_token, config.jwt_secret);
 | 
			
		||||
        jwtPayload = jwtPayload["userdetails"];
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        jwtPayload = await refresh(action);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] })
 | 
			
		||||
    if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
 | 
			
		||||
    if (user.enabled == false) { throw new UserDisabledError(); }
 | 
			
		||||
    return user;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handles soft-refreshing of access-tokens.
 | 
			
		||||
 * @param action Routing-Controllers action object that provides request and response objects among other stuff.
 | 
			
		||||
 */
 | 
			
		||||
const refresh = async (action: Action) => {
 | 
			
		||||
    let refresh_token = undefined;
 | 
			
		||||
    try {
 | 
			
		||||
        refresh_token = cookie.parse(action.request.headers["cookie"])["lfk_backend__refresh_token"];
 | 
			
		||||
    }
 | 
			
		||||
    catch {
 | 
			
		||||
        throw new IllegalJWTError();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let jwtPayload = undefined;
 | 
			
		||||
    try {
 | 
			
		||||
        jwtPayload = <any>jwt.verify(refresh_token, config.jwt_secret);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        throw new IllegalJWTError();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions', 'groups', 'groups.permissions'] })
 | 
			
		||||
    if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
 | 
			
		||||
    if (user.enabled == false) { throw new UserDisabledError(); }
 | 
			
		||||
 | 
			
		||||
    let newAccess = JwtCreator.createAccess(user);
 | 
			
		||||
    action.response.header("authorization", "Bearer " + newAccess);
 | 
			
		||||
 | 
			
		||||
    return await new JwtUser(user);
 | 
			
		||||
}
 | 
			
		||||
export default UserChecker;
 | 
			
		||||
@@ -2,13 +2,13 @@ import cookie from "cookie";
 | 
			
		||||
import * as jwt from "jsonwebtoken";
 | 
			
		||||
import { Action } from "routing-controllers";
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { config } from './config';
 | 
			
		||||
import { IllegalJWTError, NoPermissionError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from './errors/AuthError';
 | 
			
		||||
import { JwtCreator, JwtUser } from './jwtcreator';
 | 
			
		||||
import { User } from './models/entities/User';
 | 
			
		||||
import { config } from '../config';
 | 
			
		||||
import { IllegalJWTError, NoPermissionError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from '../errors/AuthError';
 | 
			
		||||
import { JwtCreator, JwtUser } from '../jwtcreator';
 | 
			
		||||
import { User } from '../models/entities/User';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handels authorisation verification via jwt's for all api endpoints using the @Authorized decorator.
 | 
			
		||||
 * Handles authentication via jwt's (Bearer authorization header) for all api endpoints using the @Authorized decorator.
 | 
			
		||||
 * @param action Routing-Controllers action object that provides request and response objects among other stuff.
 | 
			
		||||
 * @param permissions The permissions that the endpoint using @Authorized requires.
 | 
			
		||||
 */
 | 
			
		||||
@@ -43,7 +43,7 @@ const authchecker = async (action: Action, permissions: string[] | string) => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handels soft-refreshing of access-tokens.
 | 
			
		||||
 * Handles soft-refreshing of access-tokens.
 | 
			
		||||
 * @param action Routing-Controllers action object that provides request and response objects among other stuff.
 | 
			
		||||
 */
 | 
			
		||||
const refresh = async (action: Action) => {
 | 
			
		||||
@@ -1,70 +0,0 @@
 | 
			
		||||
import { IsNotEmpty, IsOptional, IsPostalCode, IsString } from 'class-validator';
 | 
			
		||||
import { config } from '../../config';
 | 
			
		||||
import { Address } from '../entities/Address';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new Address entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateAddress {
 | 
			
		||||
    /**
 | 
			
		||||
     * The newaddress's description.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    description?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new address's first line.
 | 
			
		||||
     * Containing the street and house number.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    address1: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new address's second line.
 | 
			
		||||
     * Containing optional information.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    address2?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new address's postal code.
 | 
			
		||||
     * This will get checked against the postal code syntax for the configured country.
 | 
			
		||||
     * TODO: Implement the config option. 
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    @IsPostalCode(config.postalcode_validation_countrycode)
 | 
			
		||||
    postalcode: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new address's city.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    city: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new address's country.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    country: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Address entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public toAddress(): Address {
 | 
			
		||||
        let newAddress: Address = new Address();
 | 
			
		||||
 | 
			
		||||
        newAddress.address1 = this.address1;
 | 
			
		||||
        newAddress.address2 = this.address2;
 | 
			
		||||
        newAddress.postalcode = this.postalcode;
 | 
			
		||||
        newAddress.city = this.city;
 | 
			
		||||
        newAddress.country = this.country;
 | 
			
		||||
 | 
			
		||||
        return newAddress;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,85 +0,0 @@
 | 
			
		||||
import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { config } from '../../config';
 | 
			
		||||
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
 | 
			
		||||
import { Address } from '../entities/Address';
 | 
			
		||||
import { GroupContact } from '../entities/GroupContact';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new Group entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateGroupContact {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new contact's first name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    firstname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new contact's middle name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    middlename?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new contact's last name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    lastname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new contact's address.
 | 
			
		||||
     * Must be the address's id.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    address?: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The contact's phone number.
 | 
			
		||||
     * This will be validated against the configured country phone numer syntax (default: international).
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsPhoneNumber(config.phone_validation_countrycode)
 | 
			
		||||
    phone?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The contact's email address.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsEmail()
 | 
			
		||||
    email?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the new contact's address by it's id.
 | 
			
		||||
     */
 | 
			
		||||
    public async getAddress(): Promise<Address> {
 | 
			
		||||
        if (this.address === undefined || this.address === null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        if (!isNaN(this.address)) {
 | 
			
		||||
            let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
 | 
			
		||||
            if (!address) { throw new AddressNotFoundError; }
 | 
			
		||||
            return address;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new AddressWrongTypeError;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Address entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toGroupContact(): Promise<GroupContact> {
 | 
			
		||||
        let contact: GroupContact = new GroupContact();
 | 
			
		||||
        contact.firstname = this.firstname;
 | 
			
		||||
        contact.middlename = this.middlename;
 | 
			
		||||
        contact.lastname = this.lastname;
 | 
			
		||||
        contact.email = this.email;
 | 
			
		||||
        contact.phone = this.phone;
 | 
			
		||||
        contact.address = await this.getAddress();
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,72 +0,0 @@
 | 
			
		||||
import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { config } from '../../config';
 | 
			
		||||
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
 | 
			
		||||
import { Address } from '../entities/Address';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new Participant entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export abstract class CreateParticipant {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new participant's first name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    firstname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new participant's middle name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    middlename?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new participant's last name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    lastname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new participant's phone number.
 | 
			
		||||
     * This will be validated against the configured country phone numer syntax (default: international).
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsPhoneNumber(config.phone_validation_countrycode)
 | 
			
		||||
    phone?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new participant's e-mail address.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsEmail()
 | 
			
		||||
    email?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new participant's address.
 | 
			
		||||
     * Must be of type number (address id).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    address?: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the new participant's address by it's address.
 | 
			
		||||
     */
 | 
			
		||||
    public async getAddress(): Promise<Address> {
 | 
			
		||||
        if (this.address === undefined || this.address === null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        if (!isNaN(this.address)) {
 | 
			
		||||
            let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
 | 
			
		||||
            if (!address) { throw new AddressNotFoundError; }
 | 
			
		||||
            return address;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new AddressWrongTypeError;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { GroupContactNotFoundError, GroupContactWrongTypeError } from '../../errors/GroupContactErrors';
 | 
			
		||||
import { GroupContact } from '../entities/GroupContact';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new RunnerGroup entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export abstract class CreateRunnerGroup {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new group's name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    name: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new group's contact.
 | 
			
		||||
     * Optional
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    contact?: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the new group's contact by it's id.
 | 
			
		||||
     */
 | 
			
		||||
    public async getContact(): Promise<GroupContact> {
 | 
			
		||||
        if (this.contact === undefined || this.contact === null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        if (!isNaN(this.contact)) {
 | 
			
		||||
            let contact = await getConnectionManager().get().getRepository(GroupContact).findOne({ id: this.contact });
 | 
			
		||||
            if (!contact) { throw new GroupContactNotFoundError; }
 | 
			
		||||
            return contact;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new GroupContactWrongTypeError;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,48 +0,0 @@
 | 
			
		||||
import { IsInt, IsOptional } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
 | 
			
		||||
import { Address } from '../entities/Address';
 | 
			
		||||
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
 | 
			
		||||
import { CreateRunnerGroup } from './CreateRunnerGroup';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new RunnerOrganisation entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateRunnerOrganisation extends CreateRunnerGroup {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new organisation's address.
 | 
			
		||||
     * Must be of type number (address id).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    address?: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the org's address by it's id.
 | 
			
		||||
     */
 | 
			
		||||
    public async getAddress(): Promise<Address> {
 | 
			
		||||
        if (this.address === undefined || this.address === null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        if (!isNaN(this.address)) {
 | 
			
		||||
            let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
 | 
			
		||||
            if (!address) { throw new AddressNotFoundError; }
 | 
			
		||||
            return address;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new AddressWrongTypeError;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new RunnerOrganisation entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toRunnerOrganisation(): Promise<RunnerOrganisation> {
 | 
			
		||||
        let newRunnerOrganisation: RunnerOrganisation = new RunnerOrganisation();
 | 
			
		||||
 | 
			
		||||
        newRunnerOrganisation.name = this.name;
 | 
			
		||||
        newRunnerOrganisation.contact = await this.getContact();
 | 
			
		||||
        newRunnerOrganisation.address = await this.getAddress();
 | 
			
		||||
 | 
			
		||||
        return newRunnerOrganisation;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,33 +0,0 @@
 | 
			
		||||
import { IsInt, IsNotEmpty, IsPositive, IsString } from 'class-validator';
 | 
			
		||||
import { Track } from '../entities/Track';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new Track entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateTrack {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new track's name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    name: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new track's distance in meters (must be greater than 0).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    distance: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Track entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public toTrack(): Track {
 | 
			
		||||
        let newTrack: Track = new Track();
 | 
			
		||||
 | 
			
		||||
        newTrack.name = this.name;
 | 
			
		||||
        newTrack.distance = this.distance;
 | 
			
		||||
 | 
			
		||||
        return newTrack;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { RunnerGroupNeededError } from '../../errors/RunnerErrors';
 | 
			
		||||
import { RunnerOrganisationNotFoundError } from '../../errors/RunnerOrganisationErrors';
 | 
			
		||||
import { RunnerOrganizationNotFoundError } from '../../errors/RunnerOrganizationErrors';
 | 
			
		||||
import { RunnerGroup } from '../entities/RunnerGroup';
 | 
			
		||||
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
 | 
			
		||||
import { RunnerOrganization } from '../entities/RunnerOrganization';
 | 
			
		||||
import { RunnerTeam } from '../entities/RunnerTeam';
 | 
			
		||||
import { CreateRunner } from './CreateRunner';
 | 
			
		||||
import { CreateRunner } from './create/CreateRunner';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Special class used to import runners from csv files - or json arrays created from csv to be exact.
 | 
			
		||||
@@ -78,9 +78,9 @@ export class ImportRunner {
 | 
			
		||||
        let team = await getConnectionManager().get().getRepository(RunnerTeam).findOne({ id: groupID });
 | 
			
		||||
        if (team) { return team; }
 | 
			
		||||
 | 
			
		||||
        let org = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: groupID });
 | 
			
		||||
        let org = await getConnectionManager().get().getRepository(RunnerOrganization).findOne({ id: groupID });
 | 
			
		||||
        if (!org) {
 | 
			
		||||
            throw new RunnerOrganisationNotFoundError();
 | 
			
		||||
            throw new RunnerOrganizationNotFoundError();
 | 
			
		||||
        }
 | 
			
		||||
        if (this.team === undefined) { return org; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import { config } from '../../config';
 | 
			
		||||
import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError';
 | 
			
		||||
import { JwtCreator } from "../../jwtcreator";
 | 
			
		||||
import { User } from '../entities/User';
 | 
			
		||||
import { Auth } from '../responses/ResponseAuth';
 | 
			
		||||
import { ResponseAuth } from '../responses/ResponseAuth';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to create refreshed auth credentials.
 | 
			
		||||
@@ -24,8 +24,8 @@ export class RefreshAuth {
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new auth object based on this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toAuth(): Promise<Auth> {
 | 
			
		||||
        let newAuth: Auth = new Auth();
 | 
			
		||||
    public async toAuth(): Promise<ResponseAuth> {
 | 
			
		||||
        let newAuth: ResponseAuth = new ResponseAuth();
 | 
			
		||||
        if (!this.token || this.token === undefined) {
 | 
			
		||||
            throw new JwtNotProvidedError()
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
import { IsInt, IsObject } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { RunnerGroupNotFoundError } from '../../errors/RunnerGroupErrors';
 | 
			
		||||
import { RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
 | 
			
		||||
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
 | 
			
		||||
import { Runner } from '../entities/Runner';
 | 
			
		||||
import { RunnerGroup } from '../entities/RunnerGroup';
 | 
			
		||||
import { CreateParticipant } from './CreateParticipant';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to update a Runner entity (via put request).
 | 
			
		||||
 */
 | 
			
		||||
export class UpdateRunner extends CreateParticipant {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated runner's id.
 | 
			
		||||
     * This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    id: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated runner's new team/org.
 | 
			
		||||
     * Just has to contain the group's id -everything else won't be checked or changed.
 | 
			
		||||
     */
 | 
			
		||||
    @IsObject()
 | 
			
		||||
    group: RunnerGroup;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates a provided Runner entity based on this.
 | 
			
		||||
     */
 | 
			
		||||
    public async updateRunner(runner: Runner): Promise<Runner> {
 | 
			
		||||
        runner.firstname = this.firstname;
 | 
			
		||||
        runner.middlename = this.middlename;
 | 
			
		||||
        runner.lastname = this.lastname;
 | 
			
		||||
        runner.phone = this.phone;
 | 
			
		||||
        runner.email = this.email;
 | 
			
		||||
        runner.group = await this.getGroup();
 | 
			
		||||
        runner.address = await this.getAddress();
 | 
			
		||||
 | 
			
		||||
        return runner;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Loads the updated runner's group based on it's id.
 | 
			
		||||
     */
 | 
			
		||||
    public async getGroup(): Promise<RunnerGroup> {
 | 
			
		||||
        if (this.group === undefined || this.group === null) {
 | 
			
		||||
            throw new RunnerTeamNeedsParentError();
 | 
			
		||||
        }
 | 
			
		||||
        if (!isNaN(this.group.id)) {
 | 
			
		||||
            let group = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: this.group.id });
 | 
			
		||||
            if (!group) { throw new RunnerGroupNotFoundError; }
 | 
			
		||||
            return group;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new RunnerOrganisationWrongTypeError;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
import { IsInt, IsOptional } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { AddressNotFoundError } from '../../errors/AddressErrors';
 | 
			
		||||
import { Address } from '../entities/Address';
 | 
			
		||||
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
 | 
			
		||||
import { CreateRunnerGroup } from './CreateRunnerGroup';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to update a RunnerOrganisation entity (via put request).
 | 
			
		||||
 */
 | 
			
		||||
export class UpdateRunnerOrganisation extends CreateRunnerGroup {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated orgs's id.
 | 
			
		||||
     * This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    id: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated organisation's address.
 | 
			
		||||
     * Just has to contain the address's id - everything else won't be checked or changed.
 | 
			
		||||
     * Optional.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    address?: Address;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Loads the organisation's address based on it's id.
 | 
			
		||||
     */
 | 
			
		||||
    public async getAddress(): Promise<Address> {
 | 
			
		||||
        if (this.address === undefined || this.address === null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address.id });
 | 
			
		||||
        if (!address) { throw new AddressNotFoundError; }
 | 
			
		||||
        return address;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates a provided RunnerOrganisation entity based on this.
 | 
			
		||||
     */
 | 
			
		||||
    public async updateRunnerOrganisation(organisation: RunnerOrganisation): Promise<RunnerOrganisation> {
 | 
			
		||||
 | 
			
		||||
        organisation.name = this.name;
 | 
			
		||||
        organisation.contact = await this.getContact();
 | 
			
		||||
        organisation.address = await this.getAddress();
 | 
			
		||||
 | 
			
		||||
        return organisation;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,56 +0,0 @@
 | 
			
		||||
import { IsInt, IsNotEmpty, IsObject } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
 | 
			
		||||
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
 | 
			
		||||
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
 | 
			
		||||
import { RunnerTeam } from '../entities/RunnerTeam';
 | 
			
		||||
import { CreateRunnerGroup } from './CreateRunnerGroup';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to update a RunnerTeam entity (via put request).
 | 
			
		||||
 */
 | 
			
		||||
export class UpdateRunnerTeam extends CreateRunnerGroup {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated team's id.
 | 
			
		||||
     * This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    id: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated team's parentGroup.
 | 
			
		||||
     * Just has to contain the organisation's id - everything else won't be checked or changed.
 | 
			
		||||
     */
 | 
			
		||||
    @IsObject()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    parentGroup: RunnerOrganisation;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Loads the updated teams's parentGroup based on it's id.
 | 
			
		||||
     */
 | 
			
		||||
    public async getParent(): Promise<RunnerOrganisation> {
 | 
			
		||||
        if (this.parentGroup === undefined || this.parentGroup === null) {
 | 
			
		||||
            throw new RunnerTeamNeedsParentError();
 | 
			
		||||
        }
 | 
			
		||||
        if (!isNaN(this.parentGroup.id)) {
 | 
			
		||||
            let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: this.parentGroup.id });
 | 
			
		||||
            if (!parentGroup) { throw new RunnerOrganisationNotFoundError();; }
 | 
			
		||||
            return parentGroup;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new RunnerOrganisationWrongTypeError;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates a provided RunnerTeam entity based on this.
 | 
			
		||||
     */
 | 
			
		||||
    public async updateRunnerTeam(team: RunnerTeam): Promise<RunnerTeam> {
 | 
			
		||||
 | 
			
		||||
        team.name = this.name;
 | 
			
		||||
        team.parentGroup = await this.getParent();
 | 
			
		||||
        team.contact = await this.getContact()
 | 
			
		||||
 | 
			
		||||
        return team;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import * as argon2 from "argon2";
 | 
			
		||||
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { InvalidCredentialsError, PasswordNeededError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError';
 | 
			
		||||
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
 | 
			
		||||
import { JwtCreator } from '../../jwtcreator';
 | 
			
		||||
import { User } from '../entities/User';
 | 
			
		||||
import { Auth } from '../responses/ResponseAuth';
 | 
			
		||||
import { InvalidCredentialsError, PasswordNeededError, UserDisabledError, UserNotFoundError } from '../../../errors/AuthError';
 | 
			
		||||
import { UsernameOrEmailNeededError } from '../../../errors/UserErrors';
 | 
			
		||||
import { JwtCreator } from '../../../jwtcreator';
 | 
			
		||||
import { User } from '../../entities/User';
 | 
			
		||||
import { ResponseAuth } from '../../responses/ResponseAuth';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to create auth credentials based on user credentials provided in a json body (post request).
 | 
			
		||||
@@ -42,8 +42,8 @@ export class CreateAuth {
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new auth object based on this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toAuth(): Promise<Auth> {
 | 
			
		||||
        let newAuth: Auth = new Auth();
 | 
			
		||||
    public async toAuth(): Promise<ResponseAuth> {
 | 
			
		||||
        let newAuth: ResponseAuth = new ResponseAuth();
 | 
			
		||||
 | 
			
		||||
        if (this.email === undefined && this.username === undefined) {
 | 
			
		||||
            throw new UsernameOrEmailNeededError();
 | 
			
		||||
							
								
								
									
										52
									
								
								src/models/actions/create/CreateDistanceDonation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/models/actions/create/CreateDistanceDonation.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
import { IsInt, IsPositive } from 'class-validator';
 | 
			
		||||
import { getConnection } from 'typeorm';
 | 
			
		||||
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
 | 
			
		||||
import { DistanceDonation } from '../../entities/DistanceDonation';
 | 
			
		||||
import { Runner } from '../../entities/Runner';
 | 
			
		||||
import { CreateDonation } from './CreateDonation';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to create a new FixedDonation entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateDistanceDonation extends CreateDonation {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The donation's associated runner's id.
 | 
			
		||||
     * This is important to link the runner's distance ran to the donation.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    runner: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The donation's amount per distance (full kilometer aka 1000 meters).
 | 
			
		||||
     * The unit is your currency's smallest unit (default: euro cent).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    amountPerDistance: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new FixedDonation entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(): Promise<DistanceDonation> {
 | 
			
		||||
        let newDonation = new DistanceDonation;
 | 
			
		||||
 | 
			
		||||
        newDonation.amountPerDistance = this.amountPerDistance;
 | 
			
		||||
        newDonation.donor = await this.getDonor();
 | 
			
		||||
        newDonation.runner = await this.getRunner();
 | 
			
		||||
 | 
			
		||||
        return newDonation;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets a runner based on the runner id provided via this.runner.
 | 
			
		||||
     */
 | 
			
		||||
    public async getRunner(): Promise<Runner> {
 | 
			
		||||
        const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
 | 
			
		||||
        if (!runner) {
 | 
			
		||||
            throw new RunnerNotFoundError();
 | 
			
		||||
        }
 | 
			
		||||
        return runner;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								src/models/actions/create/CreateDonation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/models/actions/create/CreateDonation.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
import { IsInt, IsPositive } from 'class-validator';
 | 
			
		||||
import { getConnection } from 'typeorm';
 | 
			
		||||
import { DonorNotFoundError } from '../../../errors/DonorErrors';
 | 
			
		||||
import { Donation } from '../../entities/Donation';
 | 
			
		||||
import { Donor } from '../../entities/Donor';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to create a new Donation entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export abstract class CreateDonation {
 | 
			
		||||
    /**
 | 
			
		||||
     * The donation's associated donor's id.
 | 
			
		||||
     * This is important to link donations to donors.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    donor: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Donation entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public abstract toEntity(): Promise<Donation>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets a donor based on the donor id provided via this.donor.
 | 
			
		||||
     */
 | 
			
		||||
    public async getDonor(): Promise<Donor> {
 | 
			
		||||
        const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor });
 | 
			
		||||
        if (!donor) {
 | 
			
		||||
            throw new DonorNotFoundError();
 | 
			
		||||
        }
 | 
			
		||||
        return donor;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								src/models/actions/create/CreateDonor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/models/actions/create/CreateDonor.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
import { IsBoolean, IsOptional } from 'class-validator';
 | 
			
		||||
import { DonorReceiptAddressNeededError } from '../../../errors/DonorErrors';
 | 
			
		||||
import { Address } from '../../entities/Address';
 | 
			
		||||
import { Donor } from '../../entities/Donor';
 | 
			
		||||
import { CreateParticipant } from './CreateParticipant';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new Donor entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateDonor extends CreateParticipant {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Does this donor need a receipt?
 | 
			
		||||
     */
 | 
			
		||||
    @IsBoolean()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    receiptNeeded?: boolean = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Donor entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(): Promise<Donor> {
 | 
			
		||||
        let newDonor: Donor = new Donor();
 | 
			
		||||
 | 
			
		||||
        newDonor.firstname = this.firstname;
 | 
			
		||||
        newDonor.middlename = this.middlename;
 | 
			
		||||
        newDonor.lastname = this.lastname;
 | 
			
		||||
        newDonor.phone = this.phone;
 | 
			
		||||
        newDonor.email = this.email;
 | 
			
		||||
        newDonor.receiptNeeded = this.receiptNeeded;
 | 
			
		||||
        newDonor.address = this.address;
 | 
			
		||||
        Address.validate(newDonor.address);
 | 
			
		||||
        if (this.receiptNeeded == true && Address.isValidAddress(newDonor.address) == false) {
 | 
			
		||||
            throw new DonorReceiptAddressNeededError()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return newDonor;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								src/models/actions/create/CreateFixedDonation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/models/actions/create/CreateFixedDonation.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
import { IsInt, IsPositive } from 'class-validator';
 | 
			
		||||
import { FixedDonation } from '../../entities/FixedDonation';
 | 
			
		||||
import { CreateDonation } from './CreateDonation';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to create a new FixedDonation entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateFixedDonation extends CreateDonation {
 | 
			
		||||
    /**
 | 
			
		||||
     * The donation's amount.
 | 
			
		||||
     * The unit is your currency's smallest unit (default: euro cent).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    amount: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new FixedDonation entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(): Promise<FixedDonation> {
 | 
			
		||||
        let newDonation = new FixedDonation;
 | 
			
		||||
 | 
			
		||||
        newDonation.amount = this.amount;
 | 
			
		||||
        newDonation.donor = await this.getDonor();
 | 
			
		||||
 | 
			
		||||
        return newDonation;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										97
									
								
								src/models/actions/create/CreateGroupContact.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/models/actions/create/CreateGroupContact.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { config } from '../../../config';
 | 
			
		||||
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
 | 
			
		||||
import { Address } from '../../entities/Address';
 | 
			
		||||
import { GroupContact } from '../../entities/GroupContact';
 | 
			
		||||
import { RunnerGroup } from '../../entities/RunnerGroup';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new GroupContact entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateGroupContact {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new contact's first name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    firstname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new contact's middle name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    middlename?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new contact's last name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    lastname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new contact's address.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsObject()
 | 
			
		||||
    address?: Address;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The contact's phone number.
 | 
			
		||||
     * This will be validated against the configured country phone numer syntax (default: international).
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsPhoneNumber(config.phone_validation_countrycode)
 | 
			
		||||
    phone?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new contact's email address.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsEmail()
 | 
			
		||||
    email?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new contacts's groups' ids.
 | 
			
		||||
     * You can provide either one groupId or an array of groupIDs.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    groups?: number[] | number
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get's all groups for this contact by their id's;
 | 
			
		||||
     */
 | 
			
		||||
    public async getGroups(): Promise<RunnerGroup[]> {
 | 
			
		||||
        if (!this.groups) { return null; }
 | 
			
		||||
        let groups = new Array<RunnerGroup>();
 | 
			
		||||
        if (!Array.isArray(this.groups)) {
 | 
			
		||||
            this.groups = [this.groups]
 | 
			
		||||
        }
 | 
			
		||||
        for (let group of this.groups) {
 | 
			
		||||
            let found = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: group });
 | 
			
		||||
            if (!found) { throw new RunnerGroupNotFoundError(); }
 | 
			
		||||
            groups.push(found);
 | 
			
		||||
        }
 | 
			
		||||
        return groups;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new GroupContact entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(): Promise<GroupContact> {
 | 
			
		||||
        let newContact: GroupContact = new GroupContact();
 | 
			
		||||
        newContact.firstname = this.firstname;
 | 
			
		||||
        newContact.middlename = this.middlename;
 | 
			
		||||
        newContact.lastname = this.lastname;
 | 
			
		||||
        newContact.email = this.email;
 | 
			
		||||
        newContact.phone = this.phone;
 | 
			
		||||
        newContact.address = this.address;
 | 
			
		||||
        Address.validate(newContact.address);
 | 
			
		||||
        newContact.groups = await this.getGroups();
 | 
			
		||||
 | 
			
		||||
        return newContact;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								src/models/actions/create/CreateParticipant.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/models/actions/create/CreateParticipant.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
 | 
			
		||||
import { config } from '../../../config';
 | 
			
		||||
import { Address } from '../../entities/Address';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new Participant entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export abstract class CreateParticipant {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new participant's first name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    firstname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new participant's middle name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    middlename?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new participant's last name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    lastname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new participant's phone number.
 | 
			
		||||
     * This will be validated against the configured country phone numer syntax (default: international).
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsPhoneNumber(config.phone_validation_countrycode)
 | 
			
		||||
    phone?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new participant's e-mail address.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsEmail()
 | 
			
		||||
    email?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new participant's address.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsObject()
 | 
			
		||||
    address?: Address;
 | 
			
		||||
}
 | 
			
		||||
@@ -4,11 +4,11 @@ import {
 | 
			
		||||
    IsNotEmpty
 | 
			
		||||
} from "class-validator";
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { PrincipalNotFoundError } from '../../errors/PrincipalErrors';
 | 
			
		||||
import { Permission } from '../entities/Permission';
 | 
			
		||||
import { Principal } from '../entities/Principal';
 | 
			
		||||
import { PermissionAction } from '../enums/PermissionAction';
 | 
			
		||||
import { PermissionTarget } from '../enums/PermissionTargets';
 | 
			
		||||
import { PrincipalNotFoundError } from '../../../errors/PrincipalErrors';
 | 
			
		||||
import { Permission } from '../../entities/Permission';
 | 
			
		||||
import { Principal } from '../../entities/Principal';
 | 
			
		||||
import { PermissionAction } from '../../enums/PermissionAction';
 | 
			
		||||
import { PermissionTarget } from '../../enums/PermissionTargets';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new Permission entity from a json body (post request).
 | 
			
		||||
@@ -39,7 +39,7 @@ export class CreatePermission {
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Permission entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toPermission(): Promise<Permission> {
 | 
			
		||||
    public async toEntity(): Promise<Permission> {
 | 
			
		||||
        let newPermission: Permission = new Permission();
 | 
			
		||||
 | 
			
		||||
        newPermission.principal = await this.getPrincipal();
 | 
			
		||||
@@ -1,39 +1,33 @@
 | 
			
		||||
import { IsEmail, IsOptional, IsString } from 'class-validator';
 | 
			
		||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { ResetAlreadyRequestedError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError';
 | 
			
		||||
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
 | 
			
		||||
import { JwtCreator } from '../../jwtcreator';
 | 
			
		||||
import { User } from '../entities/User';
 | 
			
		||||
import { ResetAlreadyRequestedError, UserDisabledError, UserNotFoundError } from '../../../errors/AuthError';
 | 
			
		||||
import { UserEmailNeededError } from '../../../errors/UserErrors';
 | 
			
		||||
import { JwtCreator } from '../../../jwtcreator';
 | 
			
		||||
import { User } from '../../entities/User';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This calss is used to create password reset tokens for users.
 | 
			
		||||
 * This class is used to create password reset tokens for users.
 | 
			
		||||
 * These password reset token can be used to set a new password for the user for the next 15mins.
 | 
			
		||||
 */
 | 
			
		||||
export class CreateResetToken {
 | 
			
		||||
    /**
 | 
			
		||||
     * The username of the user that wants to reset their password.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    username?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The email address of the user that wants to reset their password.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    @IsEmail()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    email?: string;
 | 
			
		||||
    email: string;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a password reset token based on this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toResetToken(): Promise<any> {
 | 
			
		||||
        if (this.email === undefined && this.username === undefined) {
 | 
			
		||||
            throw new UsernameOrEmailNeededError();
 | 
			
		||||
    public async toResetToken(): Promise<string> {
 | 
			
		||||
        if (!this.email) {
 | 
			
		||||
            throw new UserEmailNeededError();
 | 
			
		||||
        }
 | 
			
		||||
        let found_user = await getConnectionManager().get().getRepository(User).findOne({ where: [{ username: this.username }, { email: this.email }] });
 | 
			
		||||
        let found_user = await getConnectionManager().get().getRepository(User).findOne({ where: [{ email: this.email }] });
 | 
			
		||||
        if (!found_user) { throw new UserNotFoundError(); }
 | 
			
		||||
        if (found_user.enabled == false) { throw new UserDisabledError(); }
 | 
			
		||||
        if (found_user.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 15 * 60)) { throw new ResetAlreadyRequestedError(); }
 | 
			
		||||
@@ -43,7 +37,7 @@ export class CreateResetToken {
 | 
			
		||||
        await getConnectionManager().get().getRepository(User).save(found_user);
 | 
			
		||||
 | 
			
		||||
        //Create the reset token
 | 
			
		||||
        let reset_token = JwtCreator.createReset(found_user);
 | 
			
		||||
        let reset_token: string = JwtCreator.createReset(found_user);
 | 
			
		||||
 | 
			
		||||
        return reset_token;
 | 
			
		||||
    }
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
import { IsInt } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { RunnerGroupNotFoundError } from '../../errors/RunnerGroupErrors';
 | 
			
		||||
import { RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
 | 
			
		||||
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
 | 
			
		||||
import { Runner } from '../entities/Runner';
 | 
			
		||||
import { RunnerGroup } from '../entities/RunnerGroup';
 | 
			
		||||
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
 | 
			
		||||
import { RunnerOrganizationWrongTypeError } from '../../../errors/RunnerOrganizationErrors';
 | 
			
		||||
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
 | 
			
		||||
import { Address } from '../../entities/Address';
 | 
			
		||||
import { Runner } from '../../entities/Runner';
 | 
			
		||||
import { RunnerGroup } from '../../entities/RunnerGroup';
 | 
			
		||||
import { CreateParticipant } from './CreateParticipant';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -21,7 +22,7 @@ export class CreateRunner extends CreateParticipant {
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Runner entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toRunner(): Promise<Runner> {
 | 
			
		||||
    public async toEntity(): Promise<Runner> {
 | 
			
		||||
        let newRunner: Runner = new Runner();
 | 
			
		||||
 | 
			
		||||
        newRunner.firstname = this.firstname;
 | 
			
		||||
@@ -30,7 +31,8 @@ export class CreateRunner extends CreateParticipant {
 | 
			
		||||
        newRunner.phone = this.phone;
 | 
			
		||||
        newRunner.email = this.email;
 | 
			
		||||
        newRunner.group = await this.getGroup();
 | 
			
		||||
        newRunner.address = await this.getAddress();
 | 
			
		||||
        newRunner.address = this.address;
 | 
			
		||||
        Address.validate(newRunner.address);
 | 
			
		||||
 | 
			
		||||
        return newRunner;
 | 
			
		||||
    }
 | 
			
		||||
@@ -48,6 +50,6 @@ export class CreateRunner extends CreateParticipant {
 | 
			
		||||
            return group;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new RunnerOrganisationWrongTypeError;
 | 
			
		||||
        throw new RunnerOrganizationWrongTypeError;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								src/models/actions/create/CreateRunnerCard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/models/actions/create/CreateRunnerCard.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
 | 
			
		||||
import { getConnection } from 'typeorm';
 | 
			
		||||
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
 | 
			
		||||
import { Runner } from '../../entities/Runner';
 | 
			
		||||
import { RunnerCard } from '../../entities/RunnerCard';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new RunnerCard entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateRunnerCard {
 | 
			
		||||
    /**
 | 
			
		||||
     * The card's associated runner's id.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    runner?: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Is the new card enabled (for fraud reasons)?
 | 
			
		||||
     * Default: true
 | 
			
		||||
     */
 | 
			
		||||
    @IsBoolean()
 | 
			
		||||
    enabled: boolean = true;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new RunnerCard entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(): Promise<RunnerCard> {
 | 
			
		||||
        let newCard: RunnerCard = new RunnerCard();
 | 
			
		||||
 | 
			
		||||
        newCard.enabled = this.enabled;
 | 
			
		||||
        newCard.runner = await this.getRunner();
 | 
			
		||||
 | 
			
		||||
        return newCard;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async getRunner(): Promise<Runner> {
 | 
			
		||||
        if (!this.runner) { return null; }
 | 
			
		||||
        const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
 | 
			
		||||
        if (!runner) {
 | 
			
		||||
            throw new RunnerNotFoundError();
 | 
			
		||||
        }
 | 
			
		||||
        return runner;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								src/models/actions/create/CreateRunnerGroup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/models/actions/create/CreateRunnerGroup.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { GroupContactNotFoundError } from '../../../errors/GroupContactErrors';
 | 
			
		||||
import { GroupContact } from '../../entities/GroupContact';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new RunnerGroup entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export abstract class CreateRunnerGroup {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new group's name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    name: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new group's contact's id.
 | 
			
		||||
     * Optional
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    contact?: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the new group's contact by it's id.
 | 
			
		||||
     */
 | 
			
		||||
    public async getContact(): Promise<GroupContact> {
 | 
			
		||||
        if (!this.contact) { return null; }
 | 
			
		||||
        let contact = await getConnectionManager().get().getRepository(GroupContact).findOne({ id: this.contact });
 | 
			
		||||
        if (!contact) { throw new GroupContactNotFoundError; }
 | 
			
		||||
        return contact;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										43
									
								
								src/models/actions/create/CreateRunnerOrganization.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/models/actions/create/CreateRunnerOrganization.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
import { IsBoolean, IsObject, IsOptional } from 'class-validator';
 | 
			
		||||
import * as uuid from 'uuid';
 | 
			
		||||
import { Address } from '../../entities/Address';
 | 
			
		||||
import { RunnerOrganization } from '../../entities/RunnerOrganization';
 | 
			
		||||
import { CreateRunnerGroup } from './CreateRunnerGroup';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new RunnerOrganization entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateRunnerOrganization extends CreateRunnerGroup {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new organization's address.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsObject()
 | 
			
		||||
    address?: Address;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Is registration enabled for the new organization?
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsBoolean()
 | 
			
		||||
    registrationEnabled?: boolean = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new RunnerOrganization entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(): Promise<RunnerOrganization> {
 | 
			
		||||
        let newRunnerOrganization: RunnerOrganization = new RunnerOrganization();
 | 
			
		||||
 | 
			
		||||
        newRunnerOrganization.name = this.name;
 | 
			
		||||
        newRunnerOrganization.contact = await this.getContact();
 | 
			
		||||
        newRunnerOrganization.address = this.address;
 | 
			
		||||
        Address.validate(newRunnerOrganization.address);
 | 
			
		||||
 | 
			
		||||
        if (this.registrationEnabled) {
 | 
			
		||||
            newRunnerOrganization.key = uuid.v4().toUpperCase();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return newRunnerOrganization;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { IsInt, IsNotEmpty } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
 | 
			
		||||
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
 | 
			
		||||
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
 | 
			
		||||
import { RunnerTeam } from '../entities/RunnerTeam';
 | 
			
		||||
import { RunnerOrganizationNotFoundError } from '../../../errors/RunnerOrganizationErrors';
 | 
			
		||||
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
 | 
			
		||||
import { RunnerOrganization } from '../../entities/RunnerOrganization';
 | 
			
		||||
import { RunnerTeam } from '../../entities/RunnerTeam';
 | 
			
		||||
import { CreateRunnerGroup } from './CreateRunnerGroup';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -12,7 +12,7 @@ import { CreateRunnerGroup } from './CreateRunnerGroup';
 | 
			
		||||
export class CreateRunnerTeam extends CreateRunnerGroup {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new team's parent group (organisation).
 | 
			
		||||
     * The new team's parent org's id.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
@@ -21,28 +21,23 @@ export class CreateRunnerTeam extends CreateRunnerGroup {
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the new team's parent org based on it's id.
 | 
			
		||||
     */
 | 
			
		||||
    public async getParent(): Promise<RunnerOrganisation> {
 | 
			
		||||
    public async getParent(): Promise<RunnerOrganization> {
 | 
			
		||||
        if (this.parentGroup === undefined || this.parentGroup === null) {
 | 
			
		||||
            throw new RunnerTeamNeedsParentError();
 | 
			
		||||
        }
 | 
			
		||||
        if (!isNaN(this.parentGroup)) {
 | 
			
		||||
            let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: this.parentGroup });
 | 
			
		||||
            if (!parentGroup) { throw new RunnerOrganisationNotFoundError();; }
 | 
			
		||||
            return parentGroup;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new RunnerOrganisationWrongTypeError;
 | 
			
		||||
        let parentGroup = await getConnectionManager().get().getRepository(RunnerOrganization).findOne({ id: this.parentGroup });
 | 
			
		||||
        if (!parentGroup) { throw new RunnerOrganizationNotFoundError();; }
 | 
			
		||||
        return parentGroup;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new RunnerTeam entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toRunnerTeam(): Promise<RunnerTeam> {
 | 
			
		||||
    public async toEntity(): Promise<RunnerTeam> {
 | 
			
		||||
        let newRunnerTeam: RunnerTeam = new RunnerTeam();
 | 
			
		||||
 | 
			
		||||
        newRunnerTeam.name = this.name;
 | 
			
		||||
        newRunnerTeam.parentGroup = await this.getParent();
 | 
			
		||||
 | 
			
		||||
        newRunnerTeam.contact = await this.getContact()
 | 
			
		||||
 | 
			
		||||
        return newRunnerTeam;
 | 
			
		||||
							
								
								
									
										59
									
								
								src/models/actions/create/CreateScan.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/models/actions/create/CreateScan.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
import { IsBoolean, IsInt, IsOptional, IsPositive } from 'class-validator';
 | 
			
		||||
import { getConnection } from 'typeorm';
 | 
			
		||||
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
 | 
			
		||||
import { Runner } from '../../entities/Runner';
 | 
			
		||||
import { Scan } from '../../entities/Scan';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to create a new Scan entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export abstract class CreateScan {
 | 
			
		||||
    /**
 | 
			
		||||
     * The scan's associated runner's id.
 | 
			
		||||
     * This is important to link ran distances to runners.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    runner: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Is the scan valid (for fraud reasons).
 | 
			
		||||
     * The determination of validity will work differently for every child class.
 | 
			
		||||
     * Default: true
 | 
			
		||||
     */
 | 
			
		||||
    @IsBoolean()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    valid?: boolean = true;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The scan's distance in meters.
 | 
			
		||||
     * Can be set manually or derived from another object.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    public distance: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Scan entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(): Promise<Scan> {
 | 
			
		||||
        let newScan = new Scan();
 | 
			
		||||
 | 
			
		||||
        newScan.distance = this.distance;
 | 
			
		||||
        newScan.valid = this.valid;
 | 
			
		||||
        newScan.runner = await this.getRunner();
 | 
			
		||||
 | 
			
		||||
        return newScan;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets a runner based on the runner id provided via this.runner.
 | 
			
		||||
     */
 | 
			
		||||
    public async getRunner(): Promise<Runner> {
 | 
			
		||||
        const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
 | 
			
		||||
        if (!runner) {
 | 
			
		||||
            throw new RunnerNotFoundError();
 | 
			
		||||
        }
 | 
			
		||||
        return runner;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										64
									
								
								src/models/actions/create/CreateScanStation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/models/actions/create/CreateScanStation.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
import * as argon2 from "argon2";
 | 
			
		||||
import { IsBoolean, IsInt, IsOptional, IsPositive, IsString } from 'class-validator';
 | 
			
		||||
import crypto from 'crypto';
 | 
			
		||||
import { getConnection } from 'typeorm';
 | 
			
		||||
import * as uuid from 'uuid';
 | 
			
		||||
import { TrackNotFoundError } from '../../../errors/TrackErrors';
 | 
			
		||||
import { ScanStation } from '../../entities/ScanStation';
 | 
			
		||||
import { Track } from '../../entities/Track';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to create a new StatsClient entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateScanStation {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new station's description.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    description?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The station's associated track's id.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    track: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Is this station enabled?
 | 
			
		||||
     */
 | 
			
		||||
    @IsBoolean()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    enabled?: boolean = true;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Converts this to a ScanStation entity.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(): Promise<ScanStation> {
 | 
			
		||||
        let newStation: ScanStation = new ScanStation();
 | 
			
		||||
 | 
			
		||||
        newStation.description = this.description;
 | 
			
		||||
        newStation.enabled = this.enabled;
 | 
			
		||||
        newStation.track = await this.getTrack();
 | 
			
		||||
 | 
			
		||||
        let newUUID = uuid.v4().toUpperCase();
 | 
			
		||||
        newStation.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase();
 | 
			
		||||
        newStation.key = await argon2.hash(newStation.prefix + "." + newUUID);
 | 
			
		||||
        newStation.cleartextkey = newStation.prefix + "." + newUUID;
 | 
			
		||||
 | 
			
		||||
        return newStation;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get's a track by it's id provided via this.track.
 | 
			
		||||
     * Used to link the new station to a track.
 | 
			
		||||
     */
 | 
			
		||||
    public async getTrack(): Promise<Track> {
 | 
			
		||||
        const track = await getConnection().getRepository(Track).findOne({ id: this.track });
 | 
			
		||||
        if (!track) {
 | 
			
		||||
            throw new TrackNotFoundError();
 | 
			
		||||
        }
 | 
			
		||||
        return track;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										52
									
								
								src/models/actions/create/CreateSelfServiceCitizenRunner.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/models/actions/create/CreateSelfServiceCitizenRunner.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
import { getConnection } from 'typeorm';
 | 
			
		||||
import { RunnerEmailNeededError } from '../../../errors/RunnerErrors';
 | 
			
		||||
import { Address } from '../../entities/Address';
 | 
			
		||||
import { Runner } from '../../entities/Runner';
 | 
			
		||||
import { RunnerOrganization } from '../../entities/RunnerOrganization';
 | 
			
		||||
import { CreateParticipant } from './CreateParticipant';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new Runner entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateSelfServiceCitizenRunner extends CreateParticipant {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new runners's e-mail address.
 | 
			
		||||
     * Must be provided for email-verification to work.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    @IsEmail()
 | 
			
		||||
    email: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Runner entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(): Promise<Runner> {
 | 
			
		||||
        let newRunner: Runner = new Runner();
 | 
			
		||||
 | 
			
		||||
        newRunner.firstname = this.firstname;
 | 
			
		||||
        newRunner.middlename = this.middlename;
 | 
			
		||||
        newRunner.lastname = this.lastname;
 | 
			
		||||
        newRunner.phone = this.phone;
 | 
			
		||||
        newRunner.email = this.email;
 | 
			
		||||
 | 
			
		||||
        if (!newRunner.email) {
 | 
			
		||||
            throw new RunnerEmailNeededError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        newRunner.group = await this.getGroup();
 | 
			
		||||
        newRunner.address = this.address;
 | 
			
		||||
        Address.validate(newRunner.address);
 | 
			
		||||
 | 
			
		||||
        return newRunner;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the new runner's group by it's id.
 | 
			
		||||
     */
 | 
			
		||||
    public async getGroup(): Promise<RunnerOrganization> {
 | 
			
		||||
        return await getConnection().getRepository(RunnerOrganization).findOne({ id: 1 });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								src/models/actions/create/CreateSelfServiceRunner.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/models/actions/create/CreateSelfServiceRunner.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
import { IsInt, IsOptional } from 'class-validator';
 | 
			
		||||
import { getConnection } from 'typeorm';
 | 
			
		||||
import { RunnerTeamNotFoundError } from '../../../errors/RunnerTeamErrors';
 | 
			
		||||
import { Address } from '../../entities/Address';
 | 
			
		||||
import { Runner } from '../../entities/Runner';
 | 
			
		||||
import { RunnerGroup } from '../../entities/RunnerGroup';
 | 
			
		||||
import { RunnerTeam } from '../../entities/RunnerTeam';
 | 
			
		||||
import { CreateParticipant } from './CreateParticipant';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new Runner entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateSelfServiceRunner extends CreateParticipant {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new runner's team's id.
 | 
			
		||||
     * The team has to be a part of the runner's org.
 | 
			
		||||
     * The team property may get ignored.
 | 
			
		||||
     * If no team get's provided the runner's group will be their org.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    team?: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Runner entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(group: RunnerGroup): Promise<Runner> {
 | 
			
		||||
        let newRunner: Runner = new Runner();
 | 
			
		||||
 | 
			
		||||
        newRunner.firstname = this.firstname;
 | 
			
		||||
        newRunner.middlename = this.middlename;
 | 
			
		||||
        newRunner.lastname = this.lastname;
 | 
			
		||||
        newRunner.phone = this.phone;
 | 
			
		||||
        newRunner.email = this.email;
 | 
			
		||||
        newRunner.group = await this.getGroup(group);
 | 
			
		||||
        newRunner.address = this.address;
 | 
			
		||||
        Address.validate(newRunner.address);
 | 
			
		||||
 | 
			
		||||
        return newRunner;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the new runner's group by it's id.
 | 
			
		||||
     */
 | 
			
		||||
    public async getGroup(group: RunnerGroup): Promise<RunnerGroup> {
 | 
			
		||||
        if (!this.team) {
 | 
			
		||||
            return group;
 | 
			
		||||
        }
 | 
			
		||||
        const team = await getConnection().getRepository(RunnerTeam).findOne({ id: this.team }, { relations: ["parentGroup"] });
 | 
			
		||||
        if (!team) { throw new RunnerTeamNotFoundError(); }
 | 
			
		||||
        if (team.parentGroup.id != group.id) { throw new RunnerTeamNotFoundError(); }
 | 
			
		||||
        return team;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								src/models/actions/create/CreateStatsClient.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/models/actions/create/CreateStatsClient.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
import * as argon2 from "argon2";
 | 
			
		||||
import { IsOptional, IsString } from 'class-validator';
 | 
			
		||||
import crypto from 'crypto';
 | 
			
		||||
import * as uuid from 'uuid';
 | 
			
		||||
import { StatsClient } from '../../entities/StatsClient';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new StatsClient entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateStatsClient {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new client's description.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    description?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Converts this to a StatsClient entity.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(): Promise<StatsClient> {
 | 
			
		||||
        let newClient: StatsClient = new StatsClient();
 | 
			
		||||
 | 
			
		||||
        newClient.description = this.description;
 | 
			
		||||
 | 
			
		||||
        let newUUID = uuid.v4().toUpperCase();
 | 
			
		||||
        newClient.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase();
 | 
			
		||||
        newClient.key = await argon2.hash(newClient.prefix + "." + newUUID);
 | 
			
		||||
        newClient.cleartextkey = newClient.prefix + "." + newUUID;
 | 
			
		||||
 | 
			
		||||
        return newClient;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										46
									
								
								src/models/actions/create/CreateTrack.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/models/actions/create/CreateTrack.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator';
 | 
			
		||||
import { TrackLapTimeCantBeNegativeError } from '../../../errors/TrackErrors';
 | 
			
		||||
import { Track } from '../../entities/Track';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new Track entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateTrack {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new track's name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    name: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new track's distance in meters (must be greater than 0).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    distance: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The minimum time a runner should take to run a lap on this track (in seconds).
 | 
			
		||||
     * Will be used for fraud detection.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    minimumLapTime: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Track entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public toEntity(): Track {
 | 
			
		||||
        let newTrack: Track = new Track();
 | 
			
		||||
 | 
			
		||||
        newTrack.name = this.name;
 | 
			
		||||
        newTrack.distance = this.distance;
 | 
			
		||||
        newTrack.minimumLapTime = this.minimumLapTime;
 | 
			
		||||
        if (this.minimumLapTime < 0) {
 | 
			
		||||
            throw new TrackLapTimeCantBeNegativeError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return newTrack;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										100
									
								
								src/models/actions/create/CreateTrackScan.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/models/actions/create/CreateTrackScan.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
import { IsInt, IsOptional, IsPositive } from 'class-validator';
 | 
			
		||||
import { BadRequestError } from 'routing-controllers';
 | 
			
		||||
import { getConnection } from 'typeorm';
 | 
			
		||||
import { RunnerCardNotFoundError } from '../../../errors/RunnerCardErrors';
 | 
			
		||||
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
 | 
			
		||||
import { ScanStationNotFoundError } from '../../../errors/ScanStationErrors';
 | 
			
		||||
import { RunnerCard } from '../../entities/RunnerCard';
 | 
			
		||||
import { ScanStation } from '../../entities/ScanStation';
 | 
			
		||||
import { TrackScan } from '../../entities/TrackScan';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new Scan entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateTrackScan {
 | 
			
		||||
    /**
 | 
			
		||||
     * The id of the runnerCard associated with the scan.
 | 
			
		||||
     * This get's saved for documentation and management purposes.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    card: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The scanning station's id that created the scan.
 | 
			
		||||
     * Mainly used for logging and traceing back scans (or errors).
 | 
			
		||||
     * You don't have to provide the station if you're authenticateing via a scanstation token (The server takes care of it for you).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    station?: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Track entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(): Promise<TrackScan> {
 | 
			
		||||
        let newScan: TrackScan = new TrackScan();
 | 
			
		||||
 | 
			
		||||
        newScan.station = await this.getStation();
 | 
			
		||||
        newScan.card = await this.getCard();
 | 
			
		||||
 | 
			
		||||
        newScan.track = newScan.station.track;
 | 
			
		||||
        newScan.runner = newScan.card.runner;
 | 
			
		||||
 | 
			
		||||
        if (!newScan.runner) {
 | 
			
		||||
            throw new RunnerNotFoundError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        newScan.timestamp = Math.round(new Date().getTime() / 1000);
 | 
			
		||||
        newScan = await this.validateScan(newScan);
 | 
			
		||||
 | 
			
		||||
        return newScan;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get's a runnerCard entity via the provided id.
 | 
			
		||||
     * @returns The runnerCard whom's id you provided.
 | 
			
		||||
     */
 | 
			
		||||
    public async getCard(): Promise<RunnerCard> {
 | 
			
		||||
        const track = await getConnection().getRepository(RunnerCard).findOne({ id: this.card }, { relations: ["runner"] });
 | 
			
		||||
        if (!track) {
 | 
			
		||||
            throw new RunnerCardNotFoundError();
 | 
			
		||||
        }
 | 
			
		||||
        return track;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get's a scanstation entity via the provided id.
 | 
			
		||||
     * @returns The scanstation whom's id you provided.
 | 
			
		||||
     */
 | 
			
		||||
    public async getStation(): Promise<ScanStation> {
 | 
			
		||||
        if (!this.station) {
 | 
			
		||||
            throw new BadRequestError("You are missing the station's id!")
 | 
			
		||||
        }
 | 
			
		||||
        const station = await getConnection().getRepository(ScanStation).findOne({ id: this.station }, { relations: ["track"] });
 | 
			
		||||
        if (!station) {
 | 
			
		||||
            throw new ScanStationNotFoundError();
 | 
			
		||||
        }
 | 
			
		||||
        return station;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Validates the scan and sets it's lap time;
 | 
			
		||||
     * @param scan The scan you want to validate
 | 
			
		||||
     * @returns The validated scan with it's laptime set.
 | 
			
		||||
     */
 | 
			
		||||
    public async validateScan(scan: TrackScan): Promise<TrackScan> {
 | 
			
		||||
        const scans = await getConnection().getRepository(TrackScan).find({ where: { runner: scan.runner, valid: true }, relations: ["track"] });
 | 
			
		||||
        if (scans.length == 0) {
 | 
			
		||||
            scan.lapTime = 0;
 | 
			
		||||
            scan.valid = true;
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            const newestScan = scans[scans.length - 1];
 | 
			
		||||
            scan.lapTime = scan.timestamp - newestScan.timestamp;
 | 
			
		||||
            scan.valid = (scan.lapTime > scan.track.minimumLapTime);
 | 
			
		||||
        }
 | 
			
		||||
        return scan;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,124 +1,139 @@
 | 
			
		||||
import * as argon2 from "argon2";
 | 
			
		||||
import { IsBoolean, IsEmail, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import * as uuid from 'uuid';
 | 
			
		||||
import { config } from '../../config';
 | 
			
		||||
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
 | 
			
		||||
import { UserGroupNotFoundError } from '../../errors/UserGroupErrors';
 | 
			
		||||
import { User } from '../entities/User';
 | 
			
		||||
import { UserGroup } from '../entities/UserGroup';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new User entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateUser {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's first name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    firstname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's middle name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    middlename?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's last name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    lastname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's username.
 | 
			
		||||
     * You have to provide at least one of: {email, username}.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    username?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's email address.
 | 
			
		||||
     * You have to provide at least one of: {email, username}.
 | 
			
		||||
     */
 | 
			
		||||
    @IsEmail()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    email?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's phone number.
 | 
			
		||||
     * This will be validated against the configured country phone numer syntax (default: international).
 | 
			
		||||
     */
 | 
			
		||||
    @IsPhoneNumber(config.phone_validation_countrycode)
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    phone?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's password.
 | 
			
		||||
     * This will of course not be saved in plaintext :)
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    password: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Will the new user be enabled from the start?
 | 
			
		||||
     * Default: true
 | 
			
		||||
     */
 | 
			
		||||
    @IsBoolean()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    enabled?: boolean = true;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's groups' id(s).
 | 
			
		||||
     * You can provide either one groupId or an array of groupIDs.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    groups?: number[] | number
 | 
			
		||||
 | 
			
		||||
    //TODO: ProfilePics
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Converts this to a User entity.
 | 
			
		||||
     */
 | 
			
		||||
    public async toUser(): Promise<User> {
 | 
			
		||||
        let newUser: User = new User();
 | 
			
		||||
 | 
			
		||||
        if (this.email === undefined && this.username === undefined) {
 | 
			
		||||
            throw new UsernameOrEmailNeededError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        newUser.email = this.email
 | 
			
		||||
        newUser.username = this.username
 | 
			
		||||
        newUser.firstname = this.firstname
 | 
			
		||||
        newUser.middlename = this.middlename
 | 
			
		||||
        newUser.lastname = this.lastname
 | 
			
		||||
        newUser.uuid = uuid.v4()
 | 
			
		||||
        newUser.phone = this.phone
 | 
			
		||||
        newUser.password = await argon2.hash(this.password + newUser.uuid);
 | 
			
		||||
        newUser.groups = await this.getGroups();
 | 
			
		||||
        newUser.enabled = this.enabled;
 | 
			
		||||
        //TODO: ProfilePics
 | 
			
		||||
 | 
			
		||||
        return newUser;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get's all groups for this user by their id's;
 | 
			
		||||
     */
 | 
			
		||||
    public async getGroups() {
 | 
			
		||||
        if (!this.groups) { return null; }
 | 
			
		||||
        let groups = new Array<UserGroup>();
 | 
			
		||||
        if (!Array.isArray(this.groups)) {
 | 
			
		||||
            this.groups = [this.groups]
 | 
			
		||||
        }
 | 
			
		||||
        for (let group of this.groups) {
 | 
			
		||||
            let found = await getConnectionManager().get().getRepository(UserGroup).findOne({ id: group });
 | 
			
		||||
            if (!found) { throw new UserGroupNotFoundError(); }
 | 
			
		||||
            groups.push(found);
 | 
			
		||||
        }
 | 
			
		||||
        return groups;
 | 
			
		||||
    }
 | 
			
		||||
import * as argon2 from "argon2";
 | 
			
		||||
import { passwordStrength } from "check-password-strength";
 | 
			
		||||
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import * as uuid from 'uuid';
 | 
			
		||||
import { config } from '../../../config';
 | 
			
		||||
import { PasswordMustContainLowercaseLetterError, PasswordMustContainNumberError, PasswordMustContainUppercaseLetterError, PasswordTooShortError, UserEmailNeededError, UsernameContainsIllegalCharacterError } from '../../../errors/UserErrors';
 | 
			
		||||
import { UserGroupNotFoundError } from '../../../errors/UserGroupErrors';
 | 
			
		||||
import { User } from '../../entities/User';
 | 
			
		||||
import { UserGroup } from '../../entities/UserGroup';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new User entity from a json body (post request).
 | 
			
		||||
 */
 | 
			
		||||
export class CreateUser {
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's first name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    firstname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's middle name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    middlename?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's last name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    lastname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's username.
 | 
			
		||||
     * You have to provide a email addres, so this is optional.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    username?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's email address.
 | 
			
		||||
     */
 | 
			
		||||
    @IsEmail()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    email: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's phone number.
 | 
			
		||||
     * This will be validated against the configured country phone numer syntax (default: international).
 | 
			
		||||
     */
 | 
			
		||||
    @IsPhoneNumber(config.phone_validation_countrycode)
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    phone?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's password.
 | 
			
		||||
     * This will of course not be saved in plaintext :)
 | 
			
		||||
     */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    password: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Will the new user be enabled from the start?
 | 
			
		||||
     * Default: true
 | 
			
		||||
     */
 | 
			
		||||
    @IsBoolean()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    enabled?: boolean = true;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The new user's groups' ids.
 | 
			
		||||
     * You can provide either one groupId or an array of groupIDs.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    groups?: number[] | number
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
    * The user's profile pic (or rather a url pointing to it).
 | 
			
		||||
    */
 | 
			
		||||
    @IsString()
 | 
			
		||||
    @IsUrl()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    profilePic?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Converts this to a User entity.
 | 
			
		||||
     */
 | 
			
		||||
    public async toEntity(): Promise<User> {
 | 
			
		||||
        let newUser: User = new User();
 | 
			
		||||
 | 
			
		||||
        if (!this.email) {
 | 
			
		||||
            throw new UserEmailNeededError();
 | 
			
		||||
        }
 | 
			
		||||
        if (this.username?.includes("@")) { throw new UsernameContainsIllegalCharacterError(); }
 | 
			
		||||
 | 
			
		||||
        let password_strength = passwordStrength(this.password);
 | 
			
		||||
        if (!password_strength.contains.includes("uppercase")) { throw new PasswordMustContainUppercaseLetterError(); }
 | 
			
		||||
        if (!password_strength.contains.includes("lowercase")) { throw new PasswordMustContainLowercaseLetterError(); }
 | 
			
		||||
        if (!password_strength.contains.includes("number")) { throw new PasswordMustContainNumberError(); }
 | 
			
		||||
        if (!(password_strength.length > 9)) { throw new PasswordTooShortError(); }
 | 
			
		||||
 | 
			
		||||
        newUser.email = this.email
 | 
			
		||||
        newUser.username = this.username
 | 
			
		||||
        newUser.firstname = this.firstname
 | 
			
		||||
        newUser.middlename = this.middlename
 | 
			
		||||
        newUser.lastname = this.lastname
 | 
			
		||||
        newUser.uuid = uuid.v4()
 | 
			
		||||
        newUser.phone = this.phone
 | 
			
		||||
        newUser.password = await argon2.hash(this.password + newUser.uuid);
 | 
			
		||||
        newUser.groups = await this.getGroups();
 | 
			
		||||
        newUser.enabled = this.enabled;
 | 
			
		||||
 | 
			
		||||
        if (!this.profilePic) { newUser.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; }
 | 
			
		||||
        else { newUser.profilePic = this.profilePic; }
 | 
			
		||||
 | 
			
		||||
        return newUser;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get's all groups for this user by their id's;
 | 
			
		||||
     */
 | 
			
		||||
    public async getGroups() {
 | 
			
		||||
        if (!this.groups) { return null; }
 | 
			
		||||
        let groups = new Array<UserGroup>();
 | 
			
		||||
        if (!Array.isArray(this.groups)) {
 | 
			
		||||
            this.groups = [this.groups]
 | 
			
		||||
        }
 | 
			
		||||
        for (let group of this.groups) {
 | 
			
		||||
            let found = await getConnectionManager().get().getRepository(UserGroup).findOne({ id: group });
 | 
			
		||||
            if (!found) { throw new UserGroupNotFoundError(); }
 | 
			
		||||
            groups.push(found);
 | 
			
		||||
        }
 | 
			
		||||
        return groups;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { IsOptional, IsString } from 'class-validator';
 | 
			
		||||
import { UserGroup } from '../entities/UserGroup';
 | 
			
		||||
import { UserGroup } from '../../entities/UserGroup';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This classed is used to create a new UserGroup entity from a json body (post request).
 | 
			
		||||
@@ -22,7 +22,7 @@ export class CreateUserGroup {
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new UserGroup entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public async toUserGroup(): Promise<UserGroup> {
 | 
			
		||||
    public async toEntity(): Promise<UserGroup> {
 | 
			
		||||
        let newUserGroup: UserGroup = new UserGroup();
 | 
			
		||||
 | 
			
		||||
        newUserGroup.name = this.name;
 | 
			
		||||
							
								
								
									
										51
									
								
								src/models/actions/update/UpdateDistanceDonation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/models/actions/update/UpdateDistanceDonation.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
import { IsInt, IsPositive } from 'class-validator';
 | 
			
		||||
import { getConnection } from 'typeorm';
 | 
			
		||||
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
 | 
			
		||||
import { DistanceDonation } from '../../entities/DistanceDonation';
 | 
			
		||||
import { Runner } from '../../entities/Runner';
 | 
			
		||||
import { UpdateDonation } from './UpdateDonation';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to update a DistanceDonation entity (via put request).
 | 
			
		||||
 */
 | 
			
		||||
export class UpdateDistanceDonation extends UpdateDonation {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The donation's associated runner's id.
 | 
			
		||||
     * This is important to link the runner's distance ran to the donation.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    runner: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The donation's amount per distance (full kilometer aka 1000 meters).
 | 
			
		||||
     * The unit is your currency's smallest unit (default: euro cent).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    amountPerDistance: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update a DistanceDonation entity based on this.
 | 
			
		||||
     * @param donation The donation that shall be updated.
 | 
			
		||||
     */
 | 
			
		||||
    public async update(donation: DistanceDonation): Promise<DistanceDonation> {
 | 
			
		||||
        donation.amountPerDistance = this.amountPerDistance;
 | 
			
		||||
        donation.donor = await this.getDonor();
 | 
			
		||||
        donation.runner = await this.getRunner();
 | 
			
		||||
 | 
			
		||||
        return donation;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets a runner based on the runner id provided via this.runner.
 | 
			
		||||
     */
 | 
			
		||||
    public async getRunner(): Promise<Runner> {
 | 
			
		||||
        const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
 | 
			
		||||
        if (!runner) {
 | 
			
		||||
            throw new RunnerNotFoundError();
 | 
			
		||||
        }
 | 
			
		||||
        return runner;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/models/actions/update/UpdateDonation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/models/actions/update/UpdateDonation.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
import { IsInt, IsPositive } from 'class-validator';
 | 
			
		||||
import { getConnection } from 'typeorm';
 | 
			
		||||
import { DonorNotFoundError } from '../../../errors/DonorErrors';
 | 
			
		||||
import { Donation } from '../../entities/Donation';
 | 
			
		||||
import { Donor } from '../../entities/Donor';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to update a Donation entity (via put request).
 | 
			
		||||
 */
 | 
			
		||||
export abstract class UpdateDonation {
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated donation's id.
 | 
			
		||||
     * This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    id: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated donation's associated donor's id.
 | 
			
		||||
     * This is important to link donations to donors.
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    donor: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Donation entity from this.
 | 
			
		||||
     */
 | 
			
		||||
    public abstract update(donation: Donation): Promise<Donation>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets a donor based on the donor id provided via this.donor.
 | 
			
		||||
     */
 | 
			
		||||
    public async getDonor(): Promise<Donor> {
 | 
			
		||||
        const donor = await getConnection().getRepository(Donor).findOne({ id: this.donor });
 | 
			
		||||
        if (!donor) {
 | 
			
		||||
            throw new DonorNotFoundError();
 | 
			
		||||
        }
 | 
			
		||||
        return donor;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										46
									
								
								src/models/actions/update/UpdateDonor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/models/actions/update/UpdateDonor.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
 | 
			
		||||
import { DonorReceiptAddressNeededError } from '../../../errors/DonorErrors';
 | 
			
		||||
import { Address } from '../../entities/Address';
 | 
			
		||||
import { Donor } from '../../entities/Donor';
 | 
			
		||||
import { CreateParticipant } from '../create/CreateParticipant';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to update a Donor entity (via put request).
 | 
			
		||||
 */
 | 
			
		||||
export class UpdateDonor extends CreateParticipant {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated donor's id.
 | 
			
		||||
     * This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    id: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Does the updated donor need a receipt?
 | 
			
		||||
     */
 | 
			
		||||
    @IsBoolean()
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    receiptNeeded?: boolean;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates a provided Donor entity based on this.
 | 
			
		||||
     */
 | 
			
		||||
    public async update(donor: Donor): Promise<Donor> {
 | 
			
		||||
        donor.firstname = this.firstname;
 | 
			
		||||
        donor.middlename = this.middlename;
 | 
			
		||||
        donor.lastname = this.lastname;
 | 
			
		||||
        donor.phone = this.phone;
 | 
			
		||||
        donor.email = this.email;
 | 
			
		||||
        donor.receiptNeeded = this.receiptNeeded;
 | 
			
		||||
        if (!this.address) { donor.address.reset(); }
 | 
			
		||||
        else { donor.address = this.address; }
 | 
			
		||||
        Address.validate(donor.address);
 | 
			
		||||
        if (this.receiptNeeded == true && Address.isValidAddress(donor.address) == false) {
 | 
			
		||||
            throw new DonorReceiptAddressNeededError()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return donor;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								src/models/actions/update/UpdateFixedDonation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/models/actions/update/UpdateFixedDonation.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
import { IsInt, IsPositive } from 'class-validator';
 | 
			
		||||
import { FixedDonation } from '../../entities/FixedDonation';
 | 
			
		||||
import { UpdateDonation } from './UpdateDonation';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to update a FixedDonation entity (via put request).
 | 
			
		||||
 */
 | 
			
		||||
export class UpdateFixedDonation extends UpdateDonation {
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated donation's amount.
 | 
			
		||||
     * The unit is your currency's smallest unit (default: euro cent).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    @IsPositive()
 | 
			
		||||
    amount: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update a FixedDonation entity based on this.
 | 
			
		||||
     * @param donation The donation that shall be updated.
 | 
			
		||||
     */
 | 
			
		||||
    public async update(donation: FixedDonation): Promise<FixedDonation> {
 | 
			
		||||
        donation.amount = this.amount;
 | 
			
		||||
        donation.donor = await this.getDonor();
 | 
			
		||||
 | 
			
		||||
        return donation;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										106
									
								
								src/models/actions/update/UpdateGroupContact.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/models/actions/update/UpdateGroupContact.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
			
		||||
import { IsEmail, IsInt, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
 | 
			
		||||
import { getConnectionManager } from 'typeorm';
 | 
			
		||||
import { config } from '../../../config';
 | 
			
		||||
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
 | 
			
		||||
import { Address } from '../../entities/Address';
 | 
			
		||||
import { GroupContact } from '../../entities/GroupContact';
 | 
			
		||||
import { RunnerGroup } from '../../entities/RunnerGroup';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is used to update a GroupContact entity (via put request).
 | 
			
		||||
 */
 | 
			
		||||
export class UpdateGroupContact {
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated contact's id.
 | 
			
		||||
     * This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to).
 | 
			
		||||
     */
 | 
			
		||||
    @IsInt()
 | 
			
		||||
    id: number;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated contact's first name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    firstname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated contact's middle name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    middlename?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated contact's last name.
 | 
			
		||||
     */
 | 
			
		||||
    @IsNotEmpty()
 | 
			
		||||
    @IsString()
 | 
			
		||||
    lastname: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated contact's address.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsObject()
 | 
			
		||||
    address?: Address;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated contact's phone number.
 | 
			
		||||
     * This will be validated against the configured country phone numer syntax (default: international).
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsPhoneNumber(config.phone_validation_countrycode)
 | 
			
		||||
    phone?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated contact's email address.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    @IsEmail()
 | 
			
		||||
    email?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The updated contacts's groups' ids.
 | 
			
		||||
     * You can provide either one groupId or an array of groupIDs.
 | 
			
		||||
     */
 | 
			
		||||
    @IsOptional()
 | 
			
		||||
    groups?: number[] | number
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get's all groups for this contact by their id's;
 | 
			
		||||
     */
 | 
			
		||||
    public async getGroups(): Promise<RunnerGroup[]> {
 | 
			
		||||
        if (!this.groups) { return null; }
 | 
			
		||||
        let groups = new Array<RunnerGroup>();
 | 
			
		||||
        if (!Array.isArray(this.groups)) {
 | 
			
		||||
            this.groups = [this.groups]
 | 
			
		||||
        }
 | 
			
		||||
        for (let group of this.groups) {
 | 
			
		||||
            let found = await getConnectionManager().get().getRepository(RunnerGroup).findOne({ id: group });
 | 
			
		||||
            if (!found) { throw new RunnerGroupNotFoundError(); }
 | 
			
		||||
            groups.push(found);
 | 
			
		||||
        }
 | 
			
		||||
        return groups;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates a provided Donor entity based on this.
 | 
			
		||||
     * @param contact the contact you want to update.
 | 
			
		||||
     */
 | 
			
		||||
    public async update(contact: GroupContact): Promise<GroupContact> {
 | 
			
		||||
        contact.firstname = this.firstname; GroupContact
 | 
			
		||||
        contact.middlename = this.middlename;
 | 
			
		||||
        contact.lastname = this.lastname;
 | 
			
		||||
        contact.phone = this.phone;
 | 
			
		||||
        contact.email = this.email;
 | 
			
		||||
        if (!this.address) { contact.address.reset(); }
 | 
			
		||||
        else { contact.address = this.address; }
 | 
			
		||||
        Address.validate(contact.address);
 | 
			
		||||
        contact.groups = await this.getGroups();
 | 
			
		||||
 | 
			
		||||
        return contact;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user