Updated queries/endpoints, made admin page
This commit is contained in:
@@ -1,14 +1,31 @@
|
|||||||
RUST_LOG=debug,actix=warn,diesel_migrations=warn,reqwest=warn,hyper=warn,tracing=warn,mio=warn
|
RUST_LOG=warn,service=info
|
||||||
|
|
||||||
|
DATABASE_CONTAINER=weather-service
|
||||||
|
|
||||||
DATABASE_CONTAINER=weather-db
|
|
||||||
DATABASE_HOST=db
|
|
||||||
DATABASE_USER=weather
|
DATABASE_USER=weather
|
||||||
DATABASE_PASSWORD=
|
DATABASE_PASSWORD= # PASSWORD REQUIRED
|
||||||
DATABASE_NAME=weather
|
DATABASE_NAME=weather
|
||||||
|
DATABASE_HOST=localhost
|
||||||
DATABASE_PORT=5432
|
DATABASE_PORT=5432
|
||||||
|
|
||||||
SERVICE_HOST=service
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MINIO_ROOT_USER=siren
|
||||||
|
MINIO_ROOT_PASSWORD= # PASSWORD REQUIRED
|
||||||
|
MINIO_HOST=localhost
|
||||||
|
MINIO_PORT=9000
|
||||||
|
MINIO_PORT_INTERNAL=9001
|
||||||
|
|
||||||
|
SERVICE_HOST=localhost
|
||||||
SERVICE_PORT=5000
|
SERVICE_PORT=5000
|
||||||
|
|
||||||
UI_PORT=3000
|
ACCESS_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBME05dm5IWExKZFgzbk1hWXM4OHVvd1dRS21NSWRNMXVzbGN1MUhZdW01NWs1RE1yCm9pclBXcjcyQW5uVUhVZDczSmo2b3kzSGtYMmZrU3NGSkpVSitZdlQrL3RSRHpGdHlMWXJrbUxFVnJNbmVjVSsKeis0RHJVYitDdmkwUitXWmorMDRLdU1JdTNjSU5ONjh5ZWtQSjB4VVRQSm04bWNtT1ZGN1NJUVBxRXJKR3NtRgp2dTJZOEZGdmo5VkluK2Z3ZmRBeHJhRTEyem05WlhkWnloL2QvU05wZUgxWkVXYmVnSmhPTUJzWWlLcVhMS3V5Clc5bm5uRld2QUNTbGtoYjFLVlY0UW1TV0FVVnNnMEdTMGo5QlFrVkQ1TEZBVWpndDlzSzVDRWtxRGhpS1pNQXIKVFpWVU12eDcwTHRoZmpRNng0ZXljVEVNeG10dXRqam1kYXRQcmJFWGZleHNqNTRIVHlwSzNwSU1ncW1OVTFjNQpHYVFlcW5CSElNa0o2MTk1cUZ4WE5HejE5c2liQlkzTlpleE5HWmc0bkdGTjdrUW9QR2FWdHMyYXdiVU4xL0JZCnBjN0FpSnh5RFg5SkFweUFSUWgxcmxDVkdXb3daQ05WRkJ4OWNMTjBDeGpyYi9td0sxSkRmMHFmSms3QmpyVHcKTnVzL1k5NUp5TE1JSHNvTlpRYk1uL095N2pmMXVjV3dNUkRnYjhqSDdxa2tCQ2F3OW1md2djZVE0cVBtZzFsMgovMjVmQzh1eGlJdWRZWCtQZjBaSVVkQ09zTDllT2xYYWJGcTA4UG5jUmFuRzBFcHRsNnV6eTVuNi9waHdEK0R0Cmh1RE5ycURoNjVTUy9uU1JEVWRHbGtITms0RlByZGNRK0kraWtBZDM1RnJVb0l3ajRjT0VLa0JyT1Q4Q0F3RUEKQVFLQ0FnQnpCUkN4MnFEZ1lwQldwMzZON1YzL0pwMVcrOTQ0bU1DVk5EanpoM1g4K3E4UWxLOUFVTnlQWEFrZgpMQVNQYkVUcUtzcEZBSDZod2RVWG5kN2pXOFYyMUhNY3BqN3NZNG5adVo4ZXI1RC9RUWhKcDBFR1FGRitMVkRhCnNreDhIaGtNa3RzUnBLVzJ2Y2FqZU4zOVNvZXlXZlZGdlhDL3JkbjhVTW5jRkFLYjdUWUJyMmdnMTdnYkNJQ3YKZGdqZkxGL29yYm52cnBHQUJMb3pIaDh6bTRJb1lrMUN0YWxPVUovWHJnM0RxZWxGdnRJdkpSVEdTNjJ0Qy9XdAoyb0hwaXdQWWxOLzlrbktlbUtOQldlbUtMcFcvNzIrS2xhaWNvWjJRQTRydzZYeGs3MWVzVDc2S3Flc0xldENwCkZjNktPakwybmVUSlBQK1FmTFVyWXdSdlpNSXFKOVBVQjZUR1BIRVpsSmROQml5VlNya2d2S2R1NjllemJZZmgKQkRJeXh2Mnh4Q0pSTFU1VUJXb2I0YWp6RWlQZkhmSkIvUnNrOGdVNGNrc0Z2U0ZhZHpPU1hlNlZEYjNRR3NZNgozdFFlK2xsem5lOFVFWTg1NGg2L0JiRENWbHVEa2UxNTk5Ny8yam9MUnl0U0EySGxXc1N4MW41SFp5ZDZ1a1NpCkd4bXgvNHN6b2NGZ1FYVnhhMTljdVlIZXFSK2haa3FGaC9EYTh5UVNsOWRHYXh4WkF5RWplMzBWdjdIeEcxQ0MKQjM4RjZSUmh5Qm9LSnpRbnRNVlY2YXc2Q2FZMk43YS9hRFBLWjRONU5YY0dDKzZSRHh3b0M5bFNleXRrbkRCago1UWVIZmJMai9mRzhQWUU1NnRSWnNEZGNNVmg4SllDdk1acG1uUW9Qb0lUYU9PenNRUUtDQVFFQTl0bzZFOXhnCmZTa1NJMHpDYUdLNkQzMnBmdFJXbkR5QWJDVXpOYk5rcEJLOHJuSGlXanFJVDQzbVd6empGc0RvNlZwVXpscFQKYVVHWkNHMXc5THpHaWlaNllBd0F4TXBOZERzMFFOemhJNjMzS0tseHd0NGlUaGQ0aG9oYmZqdndGWHkyQ0paWgovUkkyZ1AwUEdvSENURXFMMTgzQklpYnJJR0g4dzY2K0F5cFc2L3cvdENEQ2NReFA1RE45YlNPSmFlQVI0a1NzCjg3REM1bmdNMVhJeVFpSCtvL21zaEpUS3ZhZUVpeTVmM1BaaExJNWZNQlZwN0tWTUNZY3V2NWZ4Y3pHVHZFM1YKcHcxamJmSzRDdG9xemFmK3hrdUk5ZWNjakp4TU5KRGc0QW5CNEpxWm11Y2dQWGJPdEpRR2VHaHZqZlBqTVZHZworTHhzSUFWZE8vRjFtUUtDQVFFQTJJeFNNK1VZOTFoem5vUURSbzV4WWVGS0dUeDhVZ00rWDdycURzTXp6NUVSCkRWKzh5WlNsY29NVjNlcGVSdjFHYlRodEUvTlZ4c1k2SW5yUkVJNHB2WFJqYkxqZDZPVkJYWENsYVl1YWsyV20KV2QxTVo4dDZRMUtVWXBFS0piZVRMN09SUmtibnIzTHhmWGJ2WTRPV1BaQjZyNktoaXljbTFubUNJU0hiMFh5Mwp1WHY1VVZEYVZWdklnS0RkNGhrRGZSWmEzNEZZUDYvcUFzMzkyWkJnclpvbVk0SkFMN2F0RnpmWVVZMUtlamV3CmpJWCtpQmRkdkd0cXQ0ZzYwQkgzQUxCZjJFb0Q4bkluaHRuUWtSd0d5QnRFN1pRVGdCYzRJbm5mR2tMZTRpWDkKQlZaSFgxb0VHWUp3RkVUNk1zUHFwcU8yWDhPT21YRDFFVFhUTUVjOGx3S0NBUUFmMWQwUG1xaEcrL2orM0hObQpDdlY3OGZUZUNueHhBY3grSmY0SXV1NEx5dTdTZ0pWMGxYL200cUlHdWo5L083bk4vbnhaY0lTNVdtQm1HZGNyCmVQMFI3QXgwUHBnS3lSeGNGUmFVRnVoaU5abGVnUnZPeWQ4YXV5UXNGWUhYTWR1d3FiakFPc080UTVVTDVaY0IKRUNNQ3U4cDFObS9sKzZidk1qUHErS3BBdGtFbmhneWhLbWhwTS9GSnVPcEFIUWtud21JTUVGZE54a29jZHZjUQp2LzJEVWVjSk5yWHRFMU5pU2l4cDFyMCtQZmdpU3VvenhVODMyY21Jb1FxQ1l4SWNqUlJFZ0xWQktoVGNwU1RmCklXdkx3aEsxZUNCZHRrU1VUY1AyTTRrTTI3VkpSaWJ4TjBXTko3bFl5STVkRVByeUQ3WUpNa0hVVWxpUGVLR2gKalc1aEFvSUJBQWdWQktSbk1vMVl3Y2Z5eVdTQ3dIeVV1ZjFESXFpMDhra0VZdVAyS1NMZ0dURFVsK2sySVE2cgpFYy9jaFhSRTA3SVQzdzVWa0tnQWtmN2pjcFlabURrMzlOWUQrRlJPNmllZ29xdlR5QXNrU2hja2lVdCticXZBCmswVXlnSnh6dzR5T09TZlVVYVZjdHVLbDQ3MWxGZUJxV2duZ0dnTmxqSytJalhETElMY3EzbmlQeGZoZytpVWgKYmRSUExMalpraVhEQmRVOXNKdC81MDMvZmkvMmtZVXBNYkdaRk9neSt6YllvTHc2ZDhNai9QVGhzMlJFNnZ5egpUYUpYOVVuNndhdEc2ZXphcGxjUUo2V0N6NlA2MWMzMkpwWnZabUxyZXU3ZWVaTXpWN285RExwOFErR3RMR1gvClZrdUxYNE14aUxwN2RiMFJRV3M4cWdqZ1oyZHY0VFVDZ2dFQkFMRjRiNnhRNjJJaCtMaTdsVk9lSWQ5VFVub08KUU1LUVNRN0xlWjJ4TmhCYWRPUEt0ZmJ5U0dGMGZieXZiVWk2czAyVnJpWC93S1V6T2o1WEFUbUZYQVdzYnU1dwo2M1JVR09ua2Z6cjIwWDZJWTVzOS9kdnJWZXFLNkpLdlQyZ0F0dWMwNXNCZzJPaG5CdHh2c0JDekhYVy9YRWJsCktWamVIMUxQTnZMaFNSc3BvT2FFVUhlaHpNN2c1V3FGSXhSQmRlb2J1SWNxQ1J2WjRFZGl6b05ybzVRZXFub3oKMTlyU0VVcTNBMEdIdE5Pb0xuV2Q3ZkZta2NOMEw5S3R0MTdsK2wxV0c3Y2kxVTVuSXBlOXBxZThlUUU2YmNYaApkNnlkdWd3UUpXbUxKSlpMQUs3eFpZdzd1ODhoa3ppZ2pSR2ltWHZ4VTJCMTU5OW5OT2NrNWQ0YXJTRT0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0=
|
||||||
NODE_ENV=development
|
ACCESS_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUEwTTl2bkhYTEpkWDNuTWFZczg4dQpvd1dRS21NSWRNMXVzbGN1MUhZdW01NWs1RE1yb2lyUFdyNzJBbm5VSFVkNzNKajZveTNIa1gyZmtTc0ZKSlVKCitZdlQrL3RSRHpGdHlMWXJrbUxFVnJNbmVjVSt6KzREclViK0N2aTBSK1daaiswNEt1TUl1M2NJTk42OHlla1AKSjB4VVRQSm04bWNtT1ZGN1NJUVBxRXJKR3NtRnZ1Mlk4RkZ2ajlWSW4rZndmZEF4cmFFMTJ6bTlaWGRaeWgvZAovU05wZUgxWkVXYmVnSmhPTUJzWWlLcVhMS3V5Vzlubm5GV3ZBQ1Nsa2hiMUtWVjRRbVNXQVVWc2cwR1MwajlCClFrVkQ1TEZBVWpndDlzSzVDRWtxRGhpS1pNQXJUWlZVTXZ4NzBMdGhmalE2eDRleWNURU14bXR1dGpqbWRhdFAKcmJFWGZleHNqNTRIVHlwSzNwSU1ncW1OVTFjNUdhUWVxbkJISU1rSjYxOTVxRnhYTkd6MTlzaWJCWTNOWmV4TgpHWmc0bkdGTjdrUW9QR2FWdHMyYXdiVU4xL0JZcGM3QWlKeHlEWDlKQXB5QVJRaDFybENWR1dvd1pDTlZGQng5CmNMTjBDeGpyYi9td0sxSkRmMHFmSms3QmpyVHdOdXMvWTk1SnlMTUlIc29OWlFiTW4vT3k3amYxdWNXd01SRGcKYjhqSDdxa2tCQ2F3OW1md2djZVE0cVBtZzFsMi8yNWZDOHV4aUl1ZFlYK1BmMFpJVWRDT3NMOWVPbFhhYkZxMAo4UG5jUmFuRzBFcHRsNnV6eTVuNi9waHdEK0R0aHVETnJxRGg2NVNTL25TUkRVZEdsa0hOazRGUHJkY1ErSStpCmtBZDM1RnJVb0l3ajRjT0VLa0JyT1Q4Q0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==
|
||||||
|
ACCESS_TOKEN_MAXAGE=5
|
||||||
|
|
||||||
|
REFRESH_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBbGEyend2WGFoL3pmdXdWN3lQY0FHTG0rY2ZXaVExUm00QWtFZW1wUjZCNmxTMThrCjhsN2Vub0puTGNLY3pLRXhsY2lQKy9uTk5oQlZuNTc2QXh3RERHdmpoRFdqTHIrZzdiUUhYVmozZVNnaG5HUVIKNlhjMTdJNGRzZXd4NVZIYlNXbitWbEt4UkZ4a0xVQVUyUWFVOTNrTFh3UlVjeFhJMngrNFFZeUkra0JGbXdndgozRjB3QXRqS1hFTmhrcmxiMjNsUTNST2l5enRhQWJwcmVYWFVWbGhwM0tGTjZHb052VFRjbEk0MitoYjlZcndDCnN2SkEwdGVxZzdqSWlOTE1XdW41QVg3VlgxOWI0VEt2TzAzc29BMWM5aDVzRHZ2anRvTmdUV25PUFNFanFqNEEKaGhnK09ZWHF0YmROWk5Ld1F3bVhCMGlzVDdwdTEyZ1g3YWU5OWNaeVhPM0dGTkpkTmcvOURCZDBuNkdQL0ZzcQpCZ2lmdkJqNTVUR3J4VDRmNGJUanYyN0xsNzc4Y1h0U2R1aThwY0dMb0E5R2RlZmViTGJCenN0TzZTMzd2ZkR2CjZNQ1oxdHlINW5aWVAzaE5rZlJ1M1pYbUlUSDVwM01zcUpwcVowUGw5ZDVlWXFidG1LMnpIVUVUT1A3bjJpSVQKR1B0RHVWeTQ3SE9QYmVISDVteTVhYzIwQUNHeUlNeXFCSUo3SXZSVDNBUm9QV1V5T2lZKzZkNXJCZjk5ZkpCdAo0NkJZOFVxZDVrcUFULzFjYjArN0NJTm5zRSszV29xYzVQOW94UW1XbnYvRjRURnQ4RUx4em13WGtmQTVhN3JoCmtKZUZqL3JjMXdCOW5sc3J4MHBsWmxwMW80cFlzQVdGQ1hrNzZCSmZiZmtsUGduTUxwbDcxc2VDekIwQ0F3RUEKQVFLQ0FnQitTT1lJTWVKRkpnZW1CWVJoRkhVU1ozVFZOWWZJQXVnaFVicGpobHpBMlVwaEEwOXE1cnd4UkpqRgpOUk9TV3RZNUo5VERwZ21MK2RBa01yK0I3QnB1V29ERlJYUCt0MU9SK25qVU80SGd5UWxDcC9PczVSV3NGbVBiCmdBckJEb1ZUdFlnUFVRbWJRZENMbFN1QnlGbmJTbGRidlkxNjVBQnBVS1BuT2lrLzZ3WlBQV01VSzlPY00walkKKzBqUndHNU9DRmMvajVla25OamQ2R2xST3ljQ0N1cVdhY29QczVzUDdnL0ZqdyszaGJvWG1jVTFNY3VibUxhWApHRXFwbGlFdys0Tkp6YmM5Rm5tdzBWQ2pXcVd3akZYSW1mWlYxaFJVSXhnWGVKTzNZOFJ4bUlwY21RdTNBTlA4CnFVRTFOY1hkYWJQeFExR09teDkxd3ErZHBnOVFpYnN0cnMxZ00wcnRVSmtTRGhjQ2Z4S1ZVekYzSFNZejZpbk4Kc1ZETjRFRGJjTXdHY3huMWh4ZkZPS0haL1hCcmNWLzA5TllPRTdzRGN2TSt5SzF0SEJvRXVscmh0SmJ5YUUyNwpNR3VCaGovbTF0OGhUN25TemdFRnhackFLWHByTzVMdzJPSWo5UmxoenMzME9GbjVDeU04a1ZNS1MxODRkcmdYCnJ3OEV5KzFxUzE0MVdUQWp0ZE9mRW85Q3kyT1U3SGM0N2pPZjdrSS9vTmdWaGQ2QXNUc01RNFRYai93Ymh1ajIKM1l0THkyeElVVVNyRlNZRU9mNFdHTlZjeHVGakk0SnR2Rk9JRTN4MXE4blJ3NkZabDBITkd5QU9SL2tNWlcydwpKZjZWNyszL05yOXJIeHFRRVBpRWEydGx1UUpxbjV0WU9NcEx3SzNhaWh4ektsUmJpUUtDQVFFQXllcUQwUFVQCkdyeWk1WTRoWG1OcGtaaDE4dldsZ2F3eEllS2YrTGd4eEswTE9ZNDk2NW9kUDFVNTFFRUdZSGxFcVVnWW9TSDkKOFY4dFROd1JwWDF6ZTE0LzdYM2NCWHZsTEEwWDRBdm4vNXFKYm9QRUhncFIvSTZyc1pxYnVHRUFhT3N5ME9abAp0aVJNRHU4VWtkTUZBL3lYYVgycWYrRG42YktIWFRtcXVrc3dhejNmNFI0RUUvNjBheTFJeU9BeERDNlJJSnFaCnhSRXJJcWttZHFzR3k5bmEvQy9RYVlBcDJNRDRjWDRkbmVhU2h6Z2hSMWgrWmROV3h4cWhqV3JwMWRkNmMrWk4KekxQcjY0Q2Fmb3c1OWdwLzJld0xOaEFSc1dyclF6OFhLeHFmUVRnN0ZLQm8zangwNWp0WUlaS0lKV0xLZGdhUwp1UklHSUQ3eGt0cFpmd0tDQVFFQXZjVTdqNGRaTGlCVi84TmwvWkwwTXRDa1pBR2ZzWGw4dU9WQU1IZW1QM3JwCjRkbDFaNnYvb05NRjZJQXVjeWlmelR4dlJLNjg1c3dJTlVmOEFKRGRwZXBDZTFjdG1INEpHZUdMWmhic1lFc3cKOEFydTRSM1BNT0dxS0Y0OEZsbktVQTZhY050TEZmZDJvNnlvY0Y0MCtaeXh3QTdhdVllR0ljNTNEZ2FoYUd4Mwo5bitVOXVLSjYzL0FLNGV3dlhsWmhYNUhzRTdmN0sxb0pxdkRMSXRGS01CSVhwaTJVOUpUME1XRm1jK2tkMTVKCjZONTZvVEppWXplNjg0VzRGbGJqSFd4OFlTQU1ad0xpZGFZOEQzVUEvV1l3czNZWDNncG5oa3gxWUJSL3hMaUoKYW1GM3c0MUpOa2Q4UHNmVXRsSmg3UHBPWEQ3aGZSdzRoNmFGMnlYUVl3S0NBUUF1MzZCR0svMmJxVnJ2aTNVMwpva0JwcWtrSFkvdE9CUmxLMG45c2orWU4wRllnd0dLamhSMXhER25tV2tvT3IxZy9MQnQ3bkphRktDRXVESkNVCktIRmNuRjZlMVc3MFh2U3VxME4xb1kzMENuNEpCOUhKWDMvMDczSHdRd0lQWllWZzFlandFZXhld2tKZDNTYWIKUzYrSVkyVUsrajlRZkhlYUN2WGRzSHR2ei9DbmxLK2FaUXR4VU5tMVg4ZmJ5aC9Zd2g2eXdQRWRqSVRGQVJ1Swp4TjFKQ1lRS3MxYmdodjR2OFd3N2ZKbUhoSFZUcXJZZkIrNGYyVlgxMXJyV1I1R05NUDZlVlVLT1dONVZ4MzhXCkRadVBBSlQ1bEJCdU5vREUvUnNzZTBMM29MQ0R4WGdCcTlOc2RBQjNTaU9GZDZ6ZmNQV3JQSTluSTBZRXlsZnUKVFg0bEFvSUJBRzdhWnVkNXpldDI4aVdjYzlpRFhtak1uaXJaRS9ydEY2RStNWmZlWE52YUpnTkxMeHpuU1VVZAozK2FuOGZwTk1jUUcySXlMY2tkenlodXR1QlJ3aXpsZk5ZU3RNVEpSOVdrTDZvMHhPTlVyTnlRUmp1Y3JyWnRGClIwdWJlSWdwM1ZlVW9EenFyTnJoR29tVDB6VUlvdk5veUNDRHpOcnh3clcrMEtiOTBvMllSeDlUK2FXYVFheXkKaklRaEdHb21GOWcySXhSbmpzREhydjVmK1h2c3d2S0NHQVJDT3NlT0ptM2U1Q01zTzB1TFphdEZRdWNrOG5vNAoxTmxxTkZYQVhaMFRnVGlQS3crRmpObml5RlRUS1VmY3lQZ2NOT2I4dHVxcGdTc2w3bGp3M3p5b1FQaVhjTHZuCldEbW9LNlp4UzBqT0VyWXArVGhISXZLQ29OQ2FMemNDZ2dFQkFJeFdyL3MvSjdIZmovc3hUU2xWamFBNHVKdEsKMTNXaW9kMnB3bFFxai9HanZra3VSYTVZQ0g5YlVNTmFqYXpTVmhYOUtkcURoSVJSTHBaTUNSMXlSUWtGb0NSSgpGbTJJbGJCaFlybWptdzEzRzNkL0xhb1RVWFlIMkZ0YnFEQlUyYmFsMEJTNlZOQkcxZUh4dVRtSys1Sk4waldUCkUzQmhtYnl5SnZjcFZ6RUJ1eXcwY1ROeGJncTcxRWRXYlpILzVRcitJSzk4VDh3NzM5N084dFY5cEZuV2MrcVcKejhWQVNiUEc1aEVJTStUbUlzRDFhV0lrWHRsemdxbTRSaFBob3diTmVMRkVkZ1YvVnc3cjBpRUlDbXFOYjVpTgoraTlEZUlZd25WY0FKZk5hV0dwaStRekd2MGl5Szk1TDI4TncrOUtSSzJVK0VnTWZnUzQ5c1lOMW44cz0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0=
|
||||||
|
REFRESH_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFsYTJ6d3ZYYWgvemZ1d1Y3eVBjQQpHTG0rY2ZXaVExUm00QWtFZW1wUjZCNmxTMThrOGw3ZW5vSm5MY0tjektFeGxjaVArL25OTmhCVm41NzZBeHdECkRHdmpoRFdqTHIrZzdiUUhYVmozZVNnaG5HUVI2WGMxN0k0ZHNld3g1VkhiU1duK1ZsS3hSRnhrTFVBVTJRYVUKOTNrTFh3UlVjeFhJMngrNFFZeUkra0JGbXdndjNGMHdBdGpLWEVOaGtybGIyM2xRM1JPaXl6dGFBYnByZVhYVQpWbGhwM0tGTjZHb052VFRjbEk0MitoYjlZcndDc3ZKQTB0ZXFnN2pJaU5MTVd1bjVBWDdWWDE5YjRUS3ZPMDNzCm9BMWM5aDVzRHZ2anRvTmdUV25PUFNFanFqNEFoaGcrT1lYcXRiZE5aTkt3UXdtWEIwaXNUN3B1MTJnWDdhZTkKOWNaeVhPM0dGTkpkTmcvOURCZDBuNkdQL0ZzcUJnaWZ2Qmo1NVRHcnhUNGY0YlRqdjI3TGw3NzhjWHRTZHVpOApwY0dMb0E5R2RlZmViTGJCenN0TzZTMzd2ZkR2Nk1DWjF0eUg1blpZUDNoTmtmUnUzWlhtSVRINXAzTXNxSnBxClowUGw5ZDVlWXFidG1LMnpIVUVUT1A3bjJpSVRHUHREdVZ5NDdIT1BiZUhINW15NWFjMjBBQ0d5SU15cUJJSjcKSXZSVDNBUm9QV1V5T2lZKzZkNXJCZjk5ZkpCdDQ2Qlk4VXFkNWtxQVQvMWNiMCs3Q0lObnNFKzNXb3FjNVA5bwp4UW1XbnYvRjRURnQ4RUx4em13WGtmQTVhN3Joa0plRmovcmMxd0I5bmxzcngwcGxabHAxbzRwWXNBV0ZDWGs3CjZCSmZiZmtsUGduTUxwbDcxc2VDekIwQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==
|
||||||
|
REFRESH_TOKEN_MAXAGE=30
|
||||||
|
|
||||||
|
GOV_API_URL=https://aviationweather.gov/cgi-bin/data
|
||||||
|
|||||||
@@ -17,7 +17,32 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${DATABASE_PORT:-5432}:5432"
|
- "${DATABASE_PORT:-5432}:5432"
|
||||||
networks:
|
networks:
|
||||||
- weather-backend
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:latest
|
||||||
|
container_name: weather-redis
|
||||||
|
ports:
|
||||||
|
- ${REDIS_PORT:-6379}:6379
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio
|
||||||
|
container_name: siren-minio
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- minio:/data
|
||||||
|
ports:
|
||||||
|
- ${MINIO_PORT:-9000}:9000
|
||||||
|
- ${MINIO_PORT_INTERNAL:-9001}:9001
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
command: server --console-address ":9001" /data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
service:
|
service:
|
||||||
@@ -30,9 +55,10 @@ services:
|
|||||||
context: service
|
context: service
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
- redis
|
||||||
networks:
|
networks:
|
||||||
- weather-frontend
|
- frontend
|
||||||
- weather-backend
|
- backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
@@ -48,13 +74,14 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- service
|
- service
|
||||||
networks:
|
networks:
|
||||||
- weather-frontend
|
- frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db:
|
db:
|
||||||
db_logs:
|
db_logs:
|
||||||
|
minio:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
weather-frontend: {}
|
frontend:
|
||||||
weather-backend: {}
|
backend:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
RUST_LOG=waren,service=info
|
RUST_LOG=warn,service=debug
|
||||||
|
|
||||||
|
DATABASE_CONTAINER=weather-service
|
||||||
|
|
||||||
DATABASE_USER=weather
|
DATABASE_USER=weather
|
||||||
DATABASE_PASSWORD=
|
DATABASE_PASSWORD=
|
||||||
@@ -9,6 +11,12 @@ DATABASE_PORT=5432
|
|||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MINIO_ROOT_USER=siren
|
||||||
|
MINIO_ROOT_PASSWORD=7LtSkxU15ix40nu
|
||||||
|
MINIO_HOST=localhost
|
||||||
|
MINIO_PORT=9000
|
||||||
|
MINIO_PORT_INTERNAL=9001
|
||||||
|
|
||||||
SERVICE_HOST=localhost
|
SERVICE_HOST=localhost
|
||||||
SERVICE_PORT=5000
|
SERVICE_PORT=5000
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
FROM rust:1.72.1-bookworm
|
# =========
|
||||||
|
# Builder
|
||||||
WORKDIR /service
|
# =========
|
||||||
USER root
|
FROM rust:bookworm as builder
|
||||||
|
WORKDIR /builder
|
||||||
|
|
||||||
COPY migrations ./migrations
|
COPY migrations ./migrations
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
COPY airport-codes.json Cargo.toml diesel.toml ./
|
COPY Cargo.toml ./
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y cmake
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
CMD ["./target/release/weather-service"]
|
# =========
|
||||||
|
# Runtime
|
||||||
|
# =========
|
||||||
|
FROM debian:bookworm-slim as runtime
|
||||||
|
WORKDIR /service
|
||||||
|
USER root
|
||||||
|
|
||||||
|
COPY --from=builder /builder/target/release/service /usr/local/bin/service
|
||||||
|
COPY --from=packages /packages /usr/bin
|
||||||
|
|
||||||
|
CMD ["service"]
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
#!make
|
#!make
|
||||||
|
SHELL := /bin/bash
|
||||||
|
|
||||||
include .env
|
include .env
|
||||||
|
|
||||||
SHELL := /bin/bash
|
|
||||||
|
|
||||||
.PHONY: help build start stop lint
|
.PHONY: help build start stop lint
|
||||||
|
|
||||||
help: ## This info
|
help: ## This info
|
||||||
@@ -11,17 +10,17 @@ help: ## This info
|
|||||||
@cat Makefile | grep -E '^[a-zA-Z\/_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
@cat Makefile | grep -E '^[a-zA-Z\/_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||||
@echo
|
@echo
|
||||||
|
|
||||||
build: ## Build Docker containers
|
build: ## Build the Docker image
|
||||||
docker compose build
|
docker compose build
|
||||||
|
|
||||||
utils: ## Start the utils
|
utils: ## Start the utils
|
||||||
docker compose up -d db
|
docker compose up -d db
|
||||||
docker compose up -d redis
|
docker compose up -d redis
|
||||||
|
|
||||||
up: ## Start Docker containers
|
up: ## Start the Docker containers
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
down: ## Stop Docker containers
|
down: ## Stop the Docker containers
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|
||||||
connect: ## Connect to the Weather DB
|
connect: ## Connect to the Weather DB
|
||||||
|
|||||||
@@ -63666,7 +63666,7 @@
|
|||||||
"local_code": "W43"
|
"local_code": "W43"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"icao": "KW45",
|
"icao": "KLUA",
|
||||||
"category": "small_airport",
|
"category": "small_airport",
|
||||||
"full_name": "Luray Caverns Airport",
|
"full_name": "Luray Caverns Airport",
|
||||||
"point":
|
"point":
|
||||||
|
|||||||
@@ -27,6 +27,21 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- backend
|
- backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
minio:
|
||||||
|
image: minio/minio
|
||||||
|
container_name: weather-minio
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- minio:/data
|
||||||
|
ports:
|
||||||
|
- ${MINIO_PORT:-9000}:9000
|
||||||
|
- ${MINIO_PORT_INTERNAL:-9001}:9001
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
command: server --console-address ":9001" /data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
service:
|
service:
|
||||||
container_name: weather-service
|
container_name: weather-service
|
||||||
@@ -54,6 +69,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
db:
|
db:
|
||||||
db_logs:
|
db_logs:
|
||||||
|
minio:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
frontend:
|
frontend:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::error_handler::ServiceError;
|
use crate::error_handler::ServiceError;
|
||||||
use crate::db::schema::airports;
|
use crate::db::schema::airports;
|
||||||
use diesel::dsl::count_star;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
// use log::trace;
|
|
||||||
use postgis_diesel::types::*;
|
use postgis_diesel::types::*;
|
||||||
use postgis_diesel::functions::*;
|
use postgis_diesel::functions::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -25,6 +25,79 @@ pub struct InsertAirport {
|
|||||||
pub point: Point
|
pub point: Point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct QueryFilters {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub icao: Option<String>,
|
||||||
|
pub bounds: Option<Polygon<Point>>,
|
||||||
|
pub category: Option<String>,
|
||||||
|
pub order_field: Option<QueryOrderField>,
|
||||||
|
pub order_by: Option<QueryOrderBy>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for QueryFilters {
|
||||||
|
fn default() -> Self {
|
||||||
|
QueryFilters {
|
||||||
|
name: None,
|
||||||
|
icao: None,
|
||||||
|
bounds: None,
|
||||||
|
category: None,
|
||||||
|
order_field: None,
|
||||||
|
order_by: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum QueryOrderBy {
|
||||||
|
Asc,
|
||||||
|
Desc
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for QueryOrderBy {
|
||||||
|
type Err = ();
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"asc" => Ok(QueryOrderBy::Asc),
|
||||||
|
"desc" => Ok(QueryOrderBy::Desc),
|
||||||
|
_ => Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum QueryOrderField {
|
||||||
|
Icao,
|
||||||
|
Name,
|
||||||
|
Category,
|
||||||
|
Continent,
|
||||||
|
Country,
|
||||||
|
Region,
|
||||||
|
Municipality,
|
||||||
|
GPS,
|
||||||
|
Iata,
|
||||||
|
Local,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for QueryOrderField {
|
||||||
|
type Err = ();
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"icao" => Ok(QueryOrderField::Icao),
|
||||||
|
"name" => Ok(QueryOrderField::Name),
|
||||||
|
"category" => Ok(QueryOrderField::Category),
|
||||||
|
"continent" => Ok(QueryOrderField::Continent),
|
||||||
|
"iso_country" => Ok(QueryOrderField::Country),
|
||||||
|
"iso_region" => Ok(QueryOrderField::Region),
|
||||||
|
"municipality" => Ok(QueryOrderField::Municipality),
|
||||||
|
"gps_code" => Ok(QueryOrderField::GPS),
|
||||||
|
"iata_code" => Ok(QueryOrderField::Iata),
|
||||||
|
"local_code" => Ok(QueryOrderField::Local),
|
||||||
|
_ => Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Queryable, QueryableByName)]
|
#[derive(Serialize, Deserialize, Queryable, QueryableByName)]
|
||||||
#[diesel(table_name = airports)]
|
#[diesel(table_name = airports)]
|
||||||
pub struct QueryAirport {
|
pub struct QueryAirport {
|
||||||
@@ -44,51 +117,95 @@ pub struct QueryAirport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl QueryAirport {
|
impl QueryAirport {
|
||||||
pub fn get_all(bounds: &Option<Polygon<Point>>, category: &Option<String>, filter: &Option<String>, order_by: bool, limit: i32, page: i32) -> Result<Vec<Self>, ServiceError> {
|
pub fn get_all(filters: &QueryFilters, limit: i32, page: i32) -> Result<Vec<Self>, ServiceError> {
|
||||||
let mut conn = db::connection()?;
|
let mut conn = db::connection()?;
|
||||||
|
|
||||||
let mut query = airports::table
|
let mut query = airports::table.limit(limit as i64).into_boxed();
|
||||||
.limit(limit as i64)
|
// Limit query to page and limit
|
||||||
.into_boxed();
|
let offset = (page - 1) * limit;
|
||||||
query = query.filter(airports::id.gt(std::cmp::max(0, page - 1) * limit));
|
query = query.offset(offset as i64);
|
||||||
|
|
||||||
if let Some(bounds) = bounds {
|
if let Some(bounds) = &filters.bounds {
|
||||||
query = query.filter(st_contains(bounds, airports::point));
|
query = query.filter(st_contains(bounds, airports::point));
|
||||||
}
|
}
|
||||||
if let Some(category) = category {
|
if let Some(category) = &filters.category {
|
||||||
query = query.filter(airports::category.eq(category));
|
query = query.filter(airports::category.eq(category));
|
||||||
}
|
}
|
||||||
if let Some(filter) = filter {
|
if let Some(icao) = &filters.icao {
|
||||||
query = query.filter(airports::icao
|
if let Some(name) = &filters.name {
|
||||||
.ilike(format!("%{}%", filter))
|
query = query.filter(
|
||||||
.or(airports::full_name.ilike(format!("%{}%", filter)))
|
airports::icao.ilike(format!("%{}%", icao)).or(
|
||||||
|
airports::full_name.ilike(format!("%{}%", name))
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
query = query.filter(airports::icao.ilike(format!("%{}%", icao)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(order_by) = &filters.order_by {
|
||||||
|
match order_by {
|
||||||
|
QueryOrderBy::Asc => {
|
||||||
|
if let Some(order_field) = &filters.order_field {
|
||||||
|
query = match order_field {
|
||||||
|
QueryOrderField::Icao => query.order(airports::icao.asc()),
|
||||||
|
QueryOrderField::Name => query.order(airports::full_name.asc()),
|
||||||
|
QueryOrderField::Category => query.order(airports::category.asc()),
|
||||||
|
QueryOrderField::Continent => query.order(airports::continent.asc()),
|
||||||
|
QueryOrderField::Country => query.order(airports::iso_country.asc()),
|
||||||
|
QueryOrderField::Region => query.order(airports::iso_region.asc()),
|
||||||
|
QueryOrderField::Municipality => query.order(airports::municipality.asc()),
|
||||||
|
QueryOrderField::GPS => query.order(airports::gps_code.asc()),
|
||||||
|
QueryOrderField::Iata => query.order(airports::iata_code.asc()),
|
||||||
|
QueryOrderField::Local => query.order(airports::local_code.asc()),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
QueryOrderBy::Desc => {
|
||||||
|
if let Some(order_field) = &filters.order_field {
|
||||||
|
query = match order_field {
|
||||||
|
QueryOrderField::Icao => query.order(airports::icao.desc()),
|
||||||
|
QueryOrderField::Name => query.order(airports::full_name.desc()),
|
||||||
|
QueryOrderField::Category => query.order(airports::category.desc()),
|
||||||
|
QueryOrderField::Continent => query.order(airports::continent.desc()),
|
||||||
|
QueryOrderField::Country => query.order(airports::iso_country.desc()),
|
||||||
|
QueryOrderField::Region => query.order(airports::iso_region.desc()),
|
||||||
|
QueryOrderField::Municipality => query.order(airports::municipality.desc()),
|
||||||
|
QueryOrderField::GPS => query.order(airports::gps_code.desc()),
|
||||||
|
QueryOrderField::Iata => query.order(airports::iata_code.desc()),
|
||||||
|
QueryOrderField::Local => query.order(airports::local_code.desc()),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if order_by {
|
|
||||||
query = query.order(airports::category.asc());
|
|
||||||
}
|
}
|
||||||
let airports: Vec<QueryAirport> = query.load::<QueryAirport>(&mut conn)?;
|
let airports: Vec<QueryAirport> = query.load::<QueryAirport>(&mut conn)?;
|
||||||
Ok(airports)
|
Ok(airports)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_count(bounds: &Option<Polygon<Point>>, category: &Option<String>, filter: &Option<String>) -> Result<i64, ServiceError> {
|
pub fn get_count(filters: &QueryFilters) -> Result<i64, ServiceError> {
|
||||||
let mut conn = db::connection()?;
|
let mut conn = db::connection()?;
|
||||||
let mut query = airports::table.select(count_star()).into_boxed();
|
let mut query = airports::table.count().into_boxed();
|
||||||
|
|
||||||
if let Some(bounds) = bounds {
|
if let Some(bounds) = &filters.bounds {
|
||||||
query = query.filter(st_contains(bounds, airports::point));
|
query = query.filter(st_contains(bounds, airports::point));
|
||||||
}
|
}
|
||||||
if let Some(category) = category {
|
if let Some(category) = &filters.category {
|
||||||
query = query.filter(airports::category.eq(category));
|
query = query.filter(airports::category.eq(category));
|
||||||
}
|
}
|
||||||
if let Some(filter) = filter {
|
if let Some(icao) = &filters.icao {
|
||||||
query = query.filter(airports::icao
|
if let Some(name) = &filters.name {
|
||||||
.ilike(format!("%{}%", filter))
|
query = query.filter(
|
||||||
.or(airports::full_name.ilike(format!("%{}%", filter)))
|
airports::icao.ilike(format!("%{}%", icao)).or(
|
||||||
|
airports::full_name.ilike(format!("%{}%", name))
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
query = query.filter(airports::icao.ilike(format!("%{}%", icao)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let count: i64 = query.first(&mut conn)?;
|
let count: i64 = query.get_result(&mut conn)?;
|
||||||
return Ok(count);
|
return Ok(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,9 +233,12 @@ pub fn update(id: i32, airport: InsertAirport) -> Result<Self, ServiceError> {
|
|||||||
Ok(airport)
|
Ok(airport)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(id: i32) -> Result<usize, ServiceError> {
|
pub fn delete(id: Option<i32>) -> Result<usize, ServiceError> {
|
||||||
let mut conn = db::connection()?;
|
let mut conn = db::connection()?;
|
||||||
let res = diesel::delete(airports::table.filter(airports::id.eq(id))).execute(&mut conn)?;
|
let res = match id {
|
||||||
|
Some(id) => diesel::delete(airports::table.filter(airports::id.eq(id))).execute(&mut conn)?,
|
||||||
|
None => diesel::delete(airports::table).execute(&mut conn)?
|
||||||
|
};
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
use crate::{airports::{InsertAirport, QueryAirport}, db::{self, Metadata}, auth::{JwtAuth, verify_role}};
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::{airports::{InsertAirport, QueryAirport, QueryFilters, QueryOrderField, QueryOrderBy}, db::{self, Response, Metadata}, auth::{JwtAuth, verify_role}};
|
||||||
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
|
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
use postgis_diesel::types::{Polygon, Point};
|
use postgis_diesel::types::{Polygon, Point};
|
||||||
@@ -6,9 +8,12 @@ use serde::{Serialize, Deserialize};
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct GetAllParameters {
|
struct GetAllParameters {
|
||||||
filter: Option<String>,
|
name: Option<String>,
|
||||||
|
icao: Option<String>,
|
||||||
bounds: Option<String>,
|
bounds: Option<String>,
|
||||||
category: Option<String>,
|
category: Option<String>,
|
||||||
|
order_field: Option<String>,
|
||||||
|
order_by: Option<String>,
|
||||||
limit: Option<i32>,
|
limit: Option<i32>,
|
||||||
page: Option<i32>
|
page: Option<i32>
|
||||||
}
|
}
|
||||||
@@ -19,20 +24,21 @@ async fn import(auth: JwtAuth) -> HttpResponse {
|
|||||||
Ok(_) => {},
|
Ok(_) => {},
|
||||||
Err(err) => return ResponseError::error_response(&err)
|
Err(err) => return ResponseError::error_response(&err)
|
||||||
};
|
};
|
||||||
db::import_data();
|
let count = db::import_data();
|
||||||
HttpResponse::Ok().body({})
|
HttpResponse::Ok().json(Response {
|
||||||
|
data: count,
|
||||||
|
meta: None
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[get("/search")]
|
||||||
pub struct AirportsResponse {
|
|
||||||
pub data: Vec<QueryAirport>,
|
|
||||||
pub meta: Metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/airports")]
|
|
||||||
async fn get_all(req: HttpRequest) -> HttpResponse {
|
async fn get_all(req: HttpRequest) -> HttpResponse {
|
||||||
let params = web::Query::<GetAllParameters>::from_query(req.query_string()).unwrap();
|
let params = web::Query::<GetAllParameters>::from_query(req.query_string()).unwrap();
|
||||||
let polygon: Option<Polygon<Point>> = match ¶ms.bounds {
|
let mut filters = QueryFilters::default();
|
||||||
|
filters.name = params.name.clone();
|
||||||
|
filters.icao = params.icao.clone();
|
||||||
|
filters.category = params.category.clone();
|
||||||
|
filters.bounds = match ¶ms.bounds {
|
||||||
Some(b) => {
|
Some(b) => {
|
||||||
let bounds: Vec<&str> = b.split(",").collect();
|
let bounds: Vec<&str> = b.split(",").collect();
|
||||||
if bounds.len() != 4 {
|
if bounds.len() != 4 {
|
||||||
@@ -77,12 +83,13 @@ async fn get_all(req: HttpRequest) -> HttpResponse {
|
|||||||
},
|
},
|
||||||
None => None
|
None => None
|
||||||
};
|
};
|
||||||
let category = match ¶ms.category {
|
|
||||||
Some(c) => Some(c.to_string()),
|
filters.order_by = match ¶ms.order_by {
|
||||||
|
Some(o) => Some(QueryOrderBy::from_str(&o).unwrap()),
|
||||||
None => None
|
None => None
|
||||||
};
|
};
|
||||||
let filter = match ¶ms.filter {
|
filters.order_field = match ¶ms.order_field {
|
||||||
Some(f) => Some(f.to_string()),
|
Some(o) => Some(QueryOrderField::from_str(&o).unwrap()),
|
||||||
None => None
|
None => None
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,16 +101,16 @@ async fn get_all(req: HttpRequest) -> HttpResponse {
|
|||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => 1
|
None => 1
|
||||||
};
|
};
|
||||||
let total = match QueryAirport::get_count(&polygon, &category, &filter) {
|
let total = match QueryAirport::get_count(&filters) {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(_) => 0
|
Err(_) => 0
|
||||||
};
|
};
|
||||||
let pages = ((total as f64) / (if limit <= 0 { 1 } else { limit} as f64)).ceil() as i64;
|
let pages = ((total as f64) / (if limit <= 0 { 1 } else { limit} as f64)).ceil() as i64;
|
||||||
|
|
||||||
match web::block(move || QueryAirport::get_all(&polygon, &category, &filter, true, limit, page)).await.unwrap() {
|
match web::block(move || QueryAirport::get_all(&filters, limit, page)).await.unwrap() {
|
||||||
Ok(a) => HttpResponse::Ok().json(AirportsResponse {
|
Ok(a) => HttpResponse::Ok().json(Response {
|
||||||
data: a,
|
data: a,
|
||||||
meta: Metadata { page, limit, pages, total }
|
meta: Some(Metadata { page, limit, pages, total })
|
||||||
}),
|
}),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("{}", err);
|
error!("{}", err);
|
||||||
@@ -112,18 +119,12 @@ async fn get_all(req: HttpRequest) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[get("/search/{icao}")]
|
||||||
pub struct AirportResponse {
|
|
||||||
pub data: QueryAirport,
|
|
||||||
pub meta: Metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/airports/{icao}")]
|
|
||||||
async fn get(icao: web::Path<String>) -> HttpResponse {
|
async fn get(icao: web::Path<String>) -> HttpResponse {
|
||||||
match QueryAirport::find(icao.into_inner()) {
|
match QueryAirport::find(icao.into_inner()) {
|
||||||
Ok(a) => HttpResponse::Ok().json(AirportResponse {
|
Ok(a) => HttpResponse::Ok().json(Response {
|
||||||
data: a,
|
data: a,
|
||||||
meta: Metadata { page: 1, limit: 1, pages: 1, total: 1 }
|
meta: Some(Metadata { page: 1, limit: 1, pages: 1, total: 1 })
|
||||||
}),
|
}),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("{}", err);
|
error!("{}", err);
|
||||||
@@ -132,7 +133,7 @@ async fn get(icao: web::Path<String>) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/airports")]
|
#[post("/create")]
|
||||||
async fn create(airport: web::Json<InsertAirport>, auth: JwtAuth) -> HttpResponse {
|
async fn create(airport: web::Json<InsertAirport>, auth: JwtAuth) -> HttpResponse {
|
||||||
let _ = match verify_role(&auth, "admin") {
|
let _ = match verify_role(&auth, "admin") {
|
||||||
Ok(_) => {},
|
Ok(_) => {},
|
||||||
@@ -147,7 +148,7 @@ async fn create(airport: web::Json<InsertAirport>, auth: JwtAuth) -> HttpRespons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/airports/{icao}")]
|
#[put("/update/{icao}")]
|
||||||
async fn update(icao: web::Path<i32>, airport: web::Json<InsertAirport>, auth: JwtAuth) -> HttpResponse {
|
async fn update(icao: web::Path<i32>, airport: web::Json<InsertAirport>, auth: JwtAuth) -> HttpResponse {
|
||||||
let _ = match verify_role(&auth, "admin") {
|
let _ = match verify_role(&auth, "admin") {
|
||||||
Ok(_) => {},
|
Ok(_) => {},
|
||||||
@@ -162,13 +163,28 @@ async fn update(icao: web::Path<i32>, airport: web::Json<InsertAirport>, auth: J
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/airports/{icao}")]
|
#[delete("/remove")]
|
||||||
async fn delete(icao: web::Path<i32>, auth: JwtAuth) -> HttpResponse {
|
async fn remove_all(auth: JwtAuth) -> HttpResponse {
|
||||||
let _ = match verify_role(&auth, "admin") {
|
let _ = match verify_role(&auth, "admin") {
|
||||||
Ok(_) => {},
|
Ok(_) => {},
|
||||||
Err(err) => return ResponseError::error_response(&err)
|
Err(err) => return ResponseError::error_response(&err)
|
||||||
};
|
};
|
||||||
match QueryAirport::delete(icao.into_inner()) {
|
match QueryAirport::delete(None) {
|
||||||
|
Ok(_) => HttpResponse::NoContent().finish(),
|
||||||
|
Err(err) => {
|
||||||
|
error!("{}", err);
|
||||||
|
err.to_http_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/remove/{icao}")]
|
||||||
|
async fn remove(icao: web::Path<i32>, auth: JwtAuth) -> HttpResponse {
|
||||||
|
let _ = match verify_role(&auth, "admin") {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(err) => return ResponseError::error_response(&err)
|
||||||
|
};
|
||||||
|
match QueryAirport::delete(Some(icao.into_inner())) {
|
||||||
Ok(_) => HttpResponse::NoContent().finish(),
|
Ok(_) => HttpResponse::NoContent().finish(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("{}", err);
|
error!("{}", err);
|
||||||
@@ -178,10 +194,13 @@ async fn delete(icao: web::Path<i32>, auth: JwtAuth) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||||
config.service(get_all);
|
config.service(web::scope("airports")
|
||||||
config.service(get);
|
.service(get_all)
|
||||||
config.service(create);
|
.service(get)
|
||||||
config.service(update);
|
.service(create)
|
||||||
config.service(delete);
|
.service(update)
|
||||||
config.service(import);
|
.service(remove)
|
||||||
|
.service(remove_all)
|
||||||
|
.service(import)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ use actix_web::{get, post, web, HttpResponse, ResponseError, cookie::{Cookie, ti
|
|||||||
use log::error;
|
use log::error;
|
||||||
use redis::AsyncCommands;
|
use redis::AsyncCommands;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use crate::error_handler::ServiceError;
|
use crate::{error_handler::ServiceError, db::Response};
|
||||||
|
|
||||||
use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token, generate_access_token, generate_refresh_token}, db};
|
use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token, generate_access_token, generate_refresh_token}, db};
|
||||||
|
|
||||||
@@ -342,7 +342,10 @@ async fn me(auth: JwtAuth) -> HttpResponse {
|
|||||||
|
|
||||||
#[get("/roles")]
|
#[get("/roles")]
|
||||||
async fn roles() -> HttpResponse {
|
async fn roles() -> HttpResponse {
|
||||||
HttpResponse::Ok().json(vec!["admin", "user"])
|
HttpResponse::Ok().json(Response {
|
||||||
|
data: vec!["admin", "user"],
|
||||||
|
meta: None
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||||
|
|||||||
@@ -59,18 +59,26 @@ pub async fn redis_async_connection() -> Result<RedisConnection, ServiceError> {
|
|||||||
Ok(conn)
|
Ok(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_data() {
|
pub fn import_data() -> i32 {
|
||||||
let path = "airport-codes.json";
|
let path = "airport-codes.json";
|
||||||
debug!("Importing data from {}", path);
|
debug!("Importing data from {}", path);
|
||||||
let contents: String = std::fs::read_to_string(path).expect("Failed to read file");
|
let contents: String = std::fs::read_to_string(path).expect("Failed to read file");
|
||||||
let airports: Vec<InsertAirport> = serde_json::from_str(&contents).expect("JSON was not well formed.");
|
let airports: Vec<InsertAirport> = serde_json::from_str(&contents).expect("JSON was not well formed.");
|
||||||
|
let mut count = 0;
|
||||||
for airport in airports {
|
for airport in airports {
|
||||||
match QueryAirport::create(airport) {
|
match QueryAirport::create(airport) {
|
||||||
Ok(_) => {},
|
Ok(_) => count += 1,
|
||||||
Err(err) => error!("Error inserting airport; {}", err)
|
Err(err) => error!("Error inserting airport; {}", err)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
debug!("Import complete");
|
debug!("Import complete");
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Response<T> {
|
||||||
|
pub data: T,
|
||||||
|
pub meta: Option<Metadata>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use tokio::time::{sleep, Duration};
|
use tokio::time::{sleep, Duration};
|
||||||
use log::{warn, debug, trace};
|
use log::{warn, debug, trace};
|
||||||
|
|
||||||
use crate::airports::QueryAirport;
|
use crate::airports::{QueryAirport, QueryFilters};
|
||||||
use crate::metars::Metar;
|
use crate::metars::Metar;
|
||||||
|
|
||||||
pub fn update_airports() {
|
pub fn update_airports() {
|
||||||
tokio::spawn(async {
|
tokio::spawn(async {
|
||||||
loop {
|
loop {
|
||||||
debug!("METAR update start");
|
debug!("METAR update start");
|
||||||
let total = match QueryAirport::get_count(&None, &None, &None) {
|
let total = match QueryAirport::get_count(&QueryFilters::default()) {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("{}", err);
|
warn!("{}", err);
|
||||||
@@ -19,7 +19,7 @@ pub fn update_airports() {
|
|||||||
let pages = ((total as f32) / (if limit <= 0 { 1 } else { limit} as f32)).ceil() as i32;
|
let pages = ((total as f32) / (if limit <= 0 { 1 } else { limit} as f32)).ceil() as i32;
|
||||||
let mut airports: Vec<QueryAirport> = vec![];
|
let mut airports: Vec<QueryAirport> = vec![];
|
||||||
for page in 1..(pages + 1) {
|
for page in 1..(pages + 1) {
|
||||||
match QueryAirport::get_all(&None, &None, &None, false, limit, page) {
|
match QueryAirport::get_all(&QueryFilters::default(), limit, page) {
|
||||||
Ok(mut a) => {
|
Ok(mut a) => {
|
||||||
airports.append(&mut a)
|
airports.append(&mut a)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use crate::auth::{JwtAuth, QueryUser, InsertUser};
|
|||||||
|
|
||||||
#[get("users/favorites")]
|
#[get("users/favorites")]
|
||||||
async fn get_favorites(auth: JwtAuth) -> HttpResponse {
|
async fn get_favorites(auth: JwtAuth) -> HttpResponse {
|
||||||
println!("{:?}", auth);
|
|
||||||
match QueryUser::get_by_email(&auth.user.email) {
|
match QueryUser::get_by_email(&auth.user.email) {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
return HttpResponse::Ok().json(user.favorites)
|
return HttpResponse::Ok().json(user.favorites)
|
||||||
|
|||||||
@@ -1,38 +1,67 @@
|
|||||||
import { Bounds, GetAirportResponse, GetAirportsResponse } from './airport.types';
|
import { AirportOrderField, Bounds, GetAirportResponse, GetAirportsResponse } from './airport.types';
|
||||||
import { getRequest } from '.';
|
import { getRequest, deleteRequest } from '.';
|
||||||
|
|
||||||
interface GetAirportProps {
|
interface GetAirportProps {
|
||||||
icao: string;
|
icao: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAirport({ icao }: GetAirportProps): Promise<GetAirportResponse> {
|
export async function getAirport({ icao }: GetAirportProps): Promise<GetAirportResponse> {
|
||||||
const response = await getRequest(`airports/${icao}`, {});
|
const response = await getRequest(`airports/search/${icao}`);
|
||||||
return response?.json() || { data: undefined };
|
return response?.json() || { data: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetAirportsProps {
|
interface GetAirportsProps {
|
||||||
bounds?: Bounds;
|
bounds?: Bounds;
|
||||||
category?: string;
|
category?: string;
|
||||||
filter?: string;
|
name?: string;
|
||||||
|
order_field?: AirportOrderField;
|
||||||
|
order_by?: 'asc' | 'desc';
|
||||||
|
icao?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAirportsCount() {
|
||||||
|
const response = await getRequest('airports/count');
|
||||||
|
return response?.json() || { data: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAirports({
|
export async function getAirports({
|
||||||
bounds,
|
bounds,
|
||||||
category,
|
category,
|
||||||
filter,
|
name,
|
||||||
|
icao,
|
||||||
|
order_field,
|
||||||
|
order_by,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
page = 1
|
page = 1
|
||||||
}: GetAirportsProps): Promise<GetAirportsResponse> {
|
}: GetAirportsProps): Promise<GetAirportsResponse> {
|
||||||
const response = await getRequest('airports', {
|
const response = await getRequest('airports/search', {
|
||||||
bounds: bounds
|
bounds: bounds
|
||||||
? `${bounds?.northEast.lat},${bounds?.northEast.lon},${bounds?.southWest.lat},${bounds?.southWest.lon}`
|
? `${bounds?.northEast.lat},${bounds?.northEast.lon},${bounds?.southWest.lat},${bounds?.southWest.lon}`
|
||||||
: undefined,
|
: undefined,
|
||||||
category: category ?? undefined,
|
category: category ?? undefined,
|
||||||
filter: filter ?? undefined,
|
name: name ?? undefined,
|
||||||
|
icao: icao ?? undefined,
|
||||||
|
order_field: order_field ?? undefined,
|
||||||
|
order_by: order_by ?? undefined,
|
||||||
limit,
|
limit,
|
||||||
page
|
page
|
||||||
});
|
});
|
||||||
return response?.json() || { data: [] };
|
return response?.json() || { data: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function removeAirport({ icao }: { icao?: string }): Promise<any> {
|
||||||
|
let response
|
||||||
|
if (icao) {
|
||||||
|
response = await deleteRequest(`airports/remove/${icao}`);
|
||||||
|
} else {
|
||||||
|
response = await deleteRequest('airports/remove');
|
||||||
|
}
|
||||||
|
return response.status == 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importAirports(): Promise<any> {
|
||||||
|
const response = await getRequest('airports/import');
|
||||||
|
return response?.json() || { data: undefined };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Metadata } from '.';
|
||||||
import { Metar } from './metar.types';
|
import { Metar } from './metar.types';
|
||||||
|
|
||||||
export enum AirportCategory {
|
export enum AirportCategory {
|
||||||
@@ -6,6 +7,19 @@ export enum AirportCategory {
|
|||||||
LARGE = 'large_airport'
|
LARGE = 'large_airport'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AirportOrderField {
|
||||||
|
ICAO = 'icao',
|
||||||
|
NAME = 'name',
|
||||||
|
CATEGORY = 'category',
|
||||||
|
CONTINENT = 'continent',
|
||||||
|
ISO_COUNTRY = 'iso_country',
|
||||||
|
ISO_REGION = 'iso_region',
|
||||||
|
MUNICIPALITY = 'municipality',
|
||||||
|
GPS_CODE = 'gps_code',
|
||||||
|
IATA_CODE = 'iata_code',
|
||||||
|
LOCAL_CODE = 'local_code',
|
||||||
|
}
|
||||||
|
|
||||||
export interface Bounds {
|
export interface Bounds {
|
||||||
northEast: Coordinate;
|
northEast: Coordinate;
|
||||||
southWest: Coordinate;
|
southWest: Coordinate;
|
||||||
@@ -20,7 +34,7 @@ export interface Airport {
|
|||||||
icao: string;
|
icao: string;
|
||||||
category: AirportCategory;
|
category: AirportCategory;
|
||||||
full_name: string;
|
full_name: string;
|
||||||
elevation_ft: string;
|
elevation_ft: number;
|
||||||
continent: string;
|
continent: string;
|
||||||
iso_country: string;
|
iso_country: string;
|
||||||
iso_region: string;
|
iso_region: string;
|
||||||
@@ -38,8 +52,10 @@ export interface Airport {
|
|||||||
|
|
||||||
export interface GetAirportResponse {
|
export interface GetAirportResponse {
|
||||||
data: Airport;
|
data: Airport;
|
||||||
|
meta: Metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetAirportsResponse {
|
export interface GetAirportsResponse {
|
||||||
data: Airport[];
|
data: Airport[];
|
||||||
|
meta: Metadata;
|
||||||
}
|
}
|
||||||
|
|||||||
28
ui/src/app/admin/page.tsx
Normal file
28
ui/src/app/admin/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Airport } from "@/api/airport.types";
|
||||||
|
import AirportTablePanel from "@/components/Admin/AirportTablePanel";
|
||||||
|
import CreateAirportPanel from "@/components/Admin/CreateAirportPanel";
|
||||||
|
import { Container, Grid } from "@mantine/core";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const [airport, setAirport] = useState<Airport | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(airport);
|
||||||
|
}, [airport]);
|
||||||
|
|
||||||
|
return <Container fluid>
|
||||||
|
<Grid p={'lg'}>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<AirportTablePanel setAirport={setAirport} />
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<CreateAirportPanel airport={airport} setAirport={setAirport} />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Container>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
117
ui/src/components/Admin/AirportTablePanel.tsx
Normal file
117
ui/src/components/Admin/AirportTablePanel.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { getAirports, importAirports, removeAirport } from "@/api/airport";
|
||||||
|
import { Airport } from "@/api/airport.types";
|
||||||
|
import { Text, Button, Card, Group, Pagination, ScrollArea, Table, TextInput, rem } from "@mantine/core";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { CiSearch } from "react-icons/ci";
|
||||||
|
|
||||||
|
|
||||||
|
export default function AirportTablePanel({ setAirport }: { setAirport: (airport: Airport) => void }) {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
const [airports, setAirports] = useState<Airport[]>([]);
|
||||||
|
|
||||||
|
async function getAirportData() {
|
||||||
|
const response = await getAirports({
|
||||||
|
page,
|
||||||
|
limit: 100
|
||||||
|
});
|
||||||
|
setAirports(response.data);
|
||||||
|
setTotalPages(response.meta.pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAirportData();
|
||||||
|
}, [page, search]);
|
||||||
|
|
||||||
|
function handleSearchChange(event: any) {
|
||||||
|
setSearch(event.currentTarget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = airports.map((airport) => (
|
||||||
|
<Table.Tr
|
||||||
|
key={airport.icao}
|
||||||
|
onClick={() => {
|
||||||
|
console.log('here');
|
||||||
|
setAirport(airport);
|
||||||
|
}}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<Table.Td>{airport.icao}</Table.Td>
|
||||||
|
<Table.Td>{airport.full_name}</Table.Td>
|
||||||
|
<Table.Td>{airport.category}</Table.Td>
|
||||||
|
<Table.Td>{airport.continent}</Table.Td>
|
||||||
|
<Table.Td>{airport.iso_country}</Table.Td>
|
||||||
|
<Table.Td>{airport.iso_region}</Table.Td>
|
||||||
|
<Table.Td>{airport.municipality}</Table.Td>
|
||||||
|
<Table.Td>{airport.gps_code}</Table.Td>
|
||||||
|
<Table.Td>{airport.iata_code}</Table.Td>
|
||||||
|
<Table.Td>{airport.local_code}</Table.Td>
|
||||||
|
<Table.Td>{airport.point.x}</Table.Td>
|
||||||
|
<Table.Td>{airport.point.y}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))
|
||||||
|
|
||||||
|
return <Card shadow={'sm'} padding={'lg'} radius={'md'} withBorder>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Search by ICAO"
|
||||||
|
mb="md"
|
||||||
|
leftSection={<CiSearch style={{ width: rem(16), height: rem(16) }} />}
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
/>
|
||||||
|
<Table.ScrollContainer minWidth={500} h={500}>
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>ICAO</Table.Th>
|
||||||
|
<Table.Th>Full Name</Table.Th>
|
||||||
|
<Table.Th>Category</Table.Th>
|
||||||
|
<Table.Th>Continent</Table.Th>
|
||||||
|
<Table.Th>ISO Country</Table.Th>
|
||||||
|
<Table.Th>ISO Region</Table.Th>
|
||||||
|
<Table.Th>Municipality</Table.Th>
|
||||||
|
<Table.Th>GPS Code</Table.Th>
|
||||||
|
<Table.Th>IATA Code</Table.Th>
|
||||||
|
<Table.Th>Local Code</Table.Th>
|
||||||
|
<Table.Th>Latitude</Table.Th>
|
||||||
|
<Table.Th>Longitude</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>{rows}</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
|
<Group>
|
||||||
|
<Pagination value={page} total={totalPages} onChange={setPage} />
|
||||||
|
<PanelButton onClick={async () => {
|
||||||
|
await importAirports();
|
||||||
|
await getAirportData();
|
||||||
|
}}>
|
||||||
|
Import
|
||||||
|
</PanelButton>
|
||||||
|
<PanelButton color={'red'} onClick={async () => {
|
||||||
|
await removeAirport({});
|
||||||
|
await getAirportData();
|
||||||
|
}}>
|
||||||
|
Remove All
|
||||||
|
</PanelButton>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
}
|
||||||
|
|
||||||
|
function PanelButton({ children, color = 'blue', onClick }: {children: any, color?: string, onClick: () => Promise<void> }) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
return <Button
|
||||||
|
loading={loading}
|
||||||
|
variant='light'
|
||||||
|
color={color}
|
||||||
|
mt={'md'}
|
||||||
|
radius={'md'}
|
||||||
|
onClick={() => {
|
||||||
|
setLoading(true);
|
||||||
|
onClick().then(() => setLoading(false));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
159
ui/src/components/Admin/CreateAirportPanel.tsx
Normal file
159
ui/src/components/Admin/CreateAirportPanel.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { Airport, AirportCategory } from "@/api/airport.types";
|
||||||
|
import { Card, TextInput, Select, Group, Flex, Space, Button } from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function CreateAirportPanel({ airport, setAirport } : { airport?: Airport, setAirport: (airport: Airport | undefined) => void }) {
|
||||||
|
const form = useForm<Airport>({
|
||||||
|
initialValues: {
|
||||||
|
icao: '',
|
||||||
|
category: AirportCategory.SMALL,
|
||||||
|
full_name: '',
|
||||||
|
elevation_ft: 0,
|
||||||
|
continent: '',
|
||||||
|
iso_country: '',
|
||||||
|
iso_region: '',
|
||||||
|
municipality: '',
|
||||||
|
gps_code: '',
|
||||||
|
iata_code: '',
|
||||||
|
local_code: '',
|
||||||
|
point: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
srid: 4326
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(airport);
|
||||||
|
if (airport) {
|
||||||
|
form.setValues(airport);
|
||||||
|
}
|
||||||
|
}, [airport]);
|
||||||
|
|
||||||
|
return <Card shadow={'sm'} padding={'lg'} radius={'md'} withBorder>
|
||||||
|
Create Airport
|
||||||
|
<form onSubmit={form.onSubmit((values) => {
|
||||||
|
if (airport) {
|
||||||
|
console.log('update');
|
||||||
|
} else {
|
||||||
|
console.log('create');
|
||||||
|
}
|
||||||
|
})}>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='ICAO'
|
||||||
|
placeholder='KHEF'
|
||||||
|
{...form.getInputProps('icao')}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
required
|
||||||
|
label='Category'
|
||||||
|
placeholder='Select category'
|
||||||
|
data={[
|
||||||
|
{ value: AirportCategory.SMALL, label: 'Small' },
|
||||||
|
{ value: AirportCategory.MEDIUM, label: 'Medium' },
|
||||||
|
{ value: AirportCategory.LARGE, label: 'Large' },
|
||||||
|
]}
|
||||||
|
{...form.getInputProps('category')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='Full Name'
|
||||||
|
placeholder='Manassas Regional Airport/Harry P. Davis Field'
|
||||||
|
{...form.getInputProps('full_name')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='Elevation (ft)'
|
||||||
|
placeholder='192'
|
||||||
|
{...form.getInputProps('elevation_ft')}
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='Continent'
|
||||||
|
placeholder='NA'
|
||||||
|
{...form.getInputProps('continent')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='ISO Country'
|
||||||
|
placeholder='US'
|
||||||
|
{...form.getInputProps('iso_country')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='ISO Region'
|
||||||
|
placeholder='US-VA'
|
||||||
|
{...form.getInputProps('iso_region')}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='Municipality'
|
||||||
|
placeholder='Manassas'
|
||||||
|
{...form.getInputProps('municipality')}
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='GPS Code'
|
||||||
|
placeholder='KHEF'
|
||||||
|
{...form.getInputProps('gps_code')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label='IATA Code'
|
||||||
|
placeholder='MNZ'
|
||||||
|
{...form.getInputProps('iata_code')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label='Local Code'
|
||||||
|
placeholder='HEF'
|
||||||
|
{...form.getInputProps('local_code')}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='Latitude'
|
||||||
|
placeholder='38.72140121'
|
||||||
|
{...form.getInputProps('point.x')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='Longitude'
|
||||||
|
placeholder='-77.51540375'
|
||||||
|
{...form.getInputProps('point.y')}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Flex justify={'end'} mt={'sm'}>
|
||||||
|
<Space mr={'sm'}>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
variant='light'
|
||||||
|
color='blue'
|
||||||
|
radius={'md'}
|
||||||
|
>
|
||||||
|
{airport ? 'Update' : 'Create'}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='light'
|
||||||
|
color='red'
|
||||||
|
radius={'md'}
|
||||||
|
onClick={() => {
|
||||||
|
form.reset();
|
||||||
|
setAirport(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { getAirports } from '@/api/airport';
|
import { getAirport, getAirports } from '@/api/airport';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Autocomplete, Avatar, Button, Card, FileButton, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core';
|
import { Autocomplete, Avatar, Button, Card, FileButton, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core';
|
||||||
import './header.css';
|
import './header.css';
|
||||||
@@ -14,6 +14,7 @@ import { getFavorites, getPicture, setPicture } from '@/api/users';
|
|||||||
import { useToggle } from '@mantine/hooks';
|
import { useToggle } from '@mantine/hooks';
|
||||||
import { HeaderModal } from './HeaderModal';
|
import { HeaderModal } from './HeaderModal';
|
||||||
import { favoritesState } from '@/state/user';
|
import { favoritesState } from '@/state/user';
|
||||||
|
import { coordinatesState, zoomState } from '@/state/map';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
@@ -24,6 +25,8 @@ export default function Header() {
|
|||||||
const [refreshId, setRefreshId] = useState<NodeJS.Timeout | undefined>(undefined);
|
const [refreshId, setRefreshId] = useState<NodeJS.Timeout | undefined>(undefined);
|
||||||
const [profilePicture, setProfilePicture] = useState<File | null>(null);
|
const [profilePicture, setProfilePicture] = useState<File | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [coordinates, setCoordinates] = useRecoilState(coordinatesState);
|
||||||
|
const [zoom, setZoom] = useRecoilState(zoomState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user || !Cookies.get('logged_in')) {
|
if (!user || !Cookies.get('logged_in')) {
|
||||||
@@ -50,7 +53,7 @@ export default function Header() {
|
|||||||
|
|
||||||
async function onChange(value: string) {
|
async function onChange(value: string) {
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
const airportData = await getAirports({ filter: value });
|
const airportData = await getAirports({ name: value, icao: value });
|
||||||
setAirports(
|
setAirports(
|
||||||
airportData.data.map((airport) => ({
|
airportData.data.map((airport) => ({
|
||||||
key: airport.icao,
|
key: airport.icao,
|
||||||
@@ -60,9 +63,11 @@ export default function Header() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClick(value: string) {
|
async function onClick(value: string) {
|
||||||
router.push(`/airport/${value}`);
|
const airport = await getAirport({ icao: value });
|
||||||
setSearchValue('');
|
if (airport) {
|
||||||
|
setCoordinates({ lat: airport.data.point.y, lon: airport.data.point.x });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { getAirports } from '@/api/airport';
|
import { getAirports } from '@/api/airport';
|
||||||
import { Airport } from '@/api/airport.types';
|
import { Airport, AirportOrderField } from '@/api/airport.types';
|
||||||
import { getMetars } from '@/api/metar';
|
import { getMetars } from '@/api/metar';
|
||||||
import { DivIcon, LatLngBounds } from 'leaflet';
|
import { DivIcon, LatLngBounds } from 'leaflet';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -9,19 +9,22 @@ import ReactDOMServer from 'react-dom/server';
|
|||||||
import { Marker, TileLayer, Tooltip, useMap, useMapEvents } from 'react-leaflet';
|
import { Marker, TileLayer, Tooltip, useMap, useMapEvents } from 'react-leaflet';
|
||||||
import MetarModal from './MetarModal';
|
import MetarModal from './MetarModal';
|
||||||
import { Avatar, MantineProvider } from '@mantine/core';
|
import { Avatar, MantineProvider } from '@mantine/core';
|
||||||
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
import { coordinatesState, zoomState } from '@/state/map';
|
||||||
|
|
||||||
export default function MapTiles() {
|
export default function MapTiles() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [airports, setAirports] = useState<Airport[]>([]);
|
const [airports, setAirports] = useState<Airport[]>([]);
|
||||||
const [selectedAirport, setSelectedAirport] = useState<Airport | undefined>();
|
const [selectedAirport, setSelectedAirport] = useState<Airport | undefined>();
|
||||||
|
const coordinates = useRecoilValue(coordinatesState);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [zoomLevel, setZoomLevel] = useState(8);
|
const [zoom, setZoom] = useRecoilState(zoomState);
|
||||||
// const [dragging, setDragging] = useState(false);
|
// const [dragging, setDragging] = useState(false);
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
|
|
||||||
const mapEvents = useMapEvents({
|
const mapEvents = useMapEvents({
|
||||||
zoomend: async () => {
|
zoomend: async () => {
|
||||||
setZoomLevel(mapEvents.getZoom());
|
setZoom(mapEvents.getZoom());
|
||||||
await updateAirports(mapEvents.getBounds());
|
await updateAirports(mapEvents.getBounds());
|
||||||
},
|
},
|
||||||
movestart: () => {
|
movestart: () => {
|
||||||
@@ -33,6 +36,10 @@ export default function MapTiles() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
map.setView([coordinates.lat, coordinates.lon]);
|
||||||
|
}, [coordinates]);
|
||||||
|
|
||||||
function handleOpen(airport: Airport) {
|
function handleOpen(airport: Airport) {
|
||||||
setSelectedAirport(airport);
|
setSelectedAirport(airport);
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
@@ -46,6 +53,8 @@ export default function MapTiles() {
|
|||||||
northEast: { lat: ne.lat, lon: ne.lng },
|
northEast: { lat: ne.lat, lon: ne.lng },
|
||||||
southWest: { lat: sw.lat, lon: sw.lng }
|
southWest: { lat: sw.lat, lon: sw.lng }
|
||||||
},
|
},
|
||||||
|
order_field: AirportOrderField.CATEGORY,
|
||||||
|
order_by: 'asc',
|
||||||
limit: 100,
|
limit: 100,
|
||||||
page: 1
|
page: 1
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { MapContainer } from 'react-leaflet';
|
import { MapContainer, useMap } from 'react-leaflet';
|
||||||
import MapTiles from './MapTiles';
|
import MapTiles from './MapTiles';
|
||||||
import './metars.css';
|
import './metars.css';
|
||||||
|
import { coordinatesState, zoomState } from '@/state/map';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
export default function Map() {
|
export default function Map() {
|
||||||
|
const coordinates = useRecoilValue(coordinatesState);
|
||||||
|
const zoom = useRecoilValue(zoomState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MapContainer
|
<MapContainer
|
||||||
center={[38.7209, -77.5133]}
|
center={[coordinates.lat, coordinates.lon]}
|
||||||
zoom={8}
|
zoom={zoom}
|
||||||
maxZoom={14} // Zoomed in
|
maxZoom={14} // Zoomed in
|
||||||
minZoom={3} // Zoomed out
|
minZoom={3} // Zoomed out
|
||||||
id='map-container'
|
id='map-container'
|
||||||
|
|||||||
12
ui/src/state/map.ts
Normal file
12
ui/src/state/map.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Coordinate } from '@/api/airport.types';
|
||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const coordinatesState = atom({
|
||||||
|
key: 'coordinatesState',
|
||||||
|
default: { lat: 38.7209, lon: -77.5133 } as Coordinate
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zoomState = atom({
|
||||||
|
key: 'zoomState',
|
||||||
|
default: 8
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user