From d45ed73eed6a323319ccbec1a66ab6bfac9dab28 Mon Sep 17 00:00:00 2001 From: Ben Sherriff Date: Mon, 20 Nov 2023 16:48:20 -0500 Subject: [PATCH] Updated queries/endpoints, made admin page --- .env.TEMPLATE | 31 ++- docker-compose.yml | 39 +++- service/.env.TEMPLATE | 10 +- service/Dockerfile | 24 ++- service/Makefile | 9 +- service/airport-codes.json | 2 +- service/docker-compose.yml | 16 ++ service/src/airports/model.rs | 188 ++++++++++++++---- service/src/airports/routes.rs | 99 +++++---- service/src/auth/routes.rs | 7 +- service/src/db/mod.rs | 12 +- service/src/scheduler.rs | 6 +- service/src/users/routes.rs | 1 - ui/src/api/airport.ts | 43 +++- ui/src/api/airport.types.ts | 18 +- ui/src/app/admin/page.tsx | 28 +++ ui/src/components/Admin/AirportTablePanel.tsx | 117 +++++++++++ .../components/Admin/CreateAirportPanel.tsx | 159 +++++++++++++++ ui/src/components/Header/index.tsx | 15 +- ui/src/components/Metars/MapTiles.tsx | 15 +- ui/src/components/Metars/MetarMap.tsx | 11 +- ui/src/state/map.ts | 12 ++ 22 files changed, 735 insertions(+), 127 deletions(-) create mode 100644 ui/src/app/admin/page.tsx create mode 100644 ui/src/components/Admin/AirportTablePanel.tsx create mode 100644 ui/src/components/Admin/CreateAirportPanel.tsx create mode 100644 ui/src/state/map.ts diff --git a/.env.TEMPLATE b/.env.TEMPLATE index d899a39..4b6abad 100644 --- a/.env.TEMPLATE +++ b/.env.TEMPLATE @@ -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_PASSWORD= +DATABASE_PASSWORD= # PASSWORD REQUIRED DATABASE_NAME=weather +DATABASE_HOST=localhost 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 -UI_PORT=3000 -NODE_ENV=development \ No newline at end of file +ACCESS_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBME05dm5IWExKZFgzbk1hWXM4OHVvd1dRS21NSWRNMXVzbGN1MUhZdW01NWs1RE1yCm9pclBXcjcyQW5uVUhVZDczSmo2b3kzSGtYMmZrU3NGSkpVSitZdlQrL3RSRHpGdHlMWXJrbUxFVnJNbmVjVSsKeis0RHJVYitDdmkwUitXWmorMDRLdU1JdTNjSU5ONjh5ZWtQSjB4VVRQSm04bWNtT1ZGN1NJUVBxRXJKR3NtRgp2dTJZOEZGdmo5VkluK2Z3ZmRBeHJhRTEyem05WlhkWnloL2QvU05wZUgxWkVXYmVnSmhPTUJzWWlLcVhMS3V5Clc5bm5uRld2QUNTbGtoYjFLVlY0UW1TV0FVVnNnMEdTMGo5QlFrVkQ1TEZBVWpndDlzSzVDRWtxRGhpS1pNQXIKVFpWVU12eDcwTHRoZmpRNng0ZXljVEVNeG10dXRqam1kYXRQcmJFWGZleHNqNTRIVHlwSzNwSU1ncW1OVTFjNQpHYVFlcW5CSElNa0o2MTk1cUZ4WE5HejE5c2liQlkzTlpleE5HWmc0bkdGTjdrUW9QR2FWdHMyYXdiVU4xL0JZCnBjN0FpSnh5RFg5SkFweUFSUWgxcmxDVkdXb3daQ05WRkJ4OWNMTjBDeGpyYi9td0sxSkRmMHFmSms3QmpyVHcKTnVzL1k5NUp5TE1JSHNvTlpRYk1uL095N2pmMXVjV3dNUkRnYjhqSDdxa2tCQ2F3OW1md2djZVE0cVBtZzFsMgovMjVmQzh1eGlJdWRZWCtQZjBaSVVkQ09zTDllT2xYYWJGcTA4UG5jUmFuRzBFcHRsNnV6eTVuNi9waHdEK0R0Cmh1RE5ycURoNjVTUy9uU1JEVWRHbGtITms0RlByZGNRK0kraWtBZDM1RnJVb0l3ajRjT0VLa0JyT1Q4Q0F3RUEKQVFLQ0FnQnpCUkN4MnFEZ1lwQldwMzZON1YzL0pwMVcrOTQ0bU1DVk5EanpoM1g4K3E4UWxLOUFVTnlQWEFrZgpMQVNQYkVUcUtzcEZBSDZod2RVWG5kN2pXOFYyMUhNY3BqN3NZNG5adVo4ZXI1RC9RUWhKcDBFR1FGRitMVkRhCnNreDhIaGtNa3RzUnBLVzJ2Y2FqZU4zOVNvZXlXZlZGdlhDL3JkbjhVTW5jRkFLYjdUWUJyMmdnMTdnYkNJQ3YKZGdqZkxGL29yYm52cnBHQUJMb3pIaDh6bTRJb1lrMUN0YWxPVUovWHJnM0RxZWxGdnRJdkpSVEdTNjJ0Qy9XdAoyb0hwaXdQWWxOLzlrbktlbUtOQldlbUtMcFcvNzIrS2xhaWNvWjJRQTRydzZYeGs3MWVzVDc2S3Flc0xldENwCkZjNktPakwybmVUSlBQK1FmTFVyWXdSdlpNSXFKOVBVQjZUR1BIRVpsSmROQml5VlNya2d2S2R1NjllemJZZmgKQkRJeXh2Mnh4Q0pSTFU1VUJXb2I0YWp6RWlQZkhmSkIvUnNrOGdVNGNrc0Z2U0ZhZHpPU1hlNlZEYjNRR3NZNgozdFFlK2xsem5lOFVFWTg1NGg2L0JiRENWbHVEa2UxNTk5Ny8yam9MUnl0U0EySGxXc1N4MW41SFp5ZDZ1a1NpCkd4bXgvNHN6b2NGZ1FYVnhhMTljdVlIZXFSK2haa3FGaC9EYTh5UVNsOWRHYXh4WkF5RWplMzBWdjdIeEcxQ0MKQjM4RjZSUmh5Qm9LSnpRbnRNVlY2YXc2Q2FZMk43YS9hRFBLWjRONU5YY0dDKzZSRHh3b0M5bFNleXRrbkRCago1UWVIZmJMai9mRzhQWUU1NnRSWnNEZGNNVmg4SllDdk1acG1uUW9Qb0lUYU9PenNRUUtDQVFFQTl0bzZFOXhnCmZTa1NJMHpDYUdLNkQzMnBmdFJXbkR5QWJDVXpOYk5rcEJLOHJuSGlXanFJVDQzbVd6empGc0RvNlZwVXpscFQKYVVHWkNHMXc5THpHaWlaNllBd0F4TXBOZERzMFFOemhJNjMzS0tseHd0NGlUaGQ0aG9oYmZqdndGWHkyQ0paWgovUkkyZ1AwUEdvSENURXFMMTgzQklpYnJJR0g4dzY2K0F5cFc2L3cvdENEQ2NReFA1RE45YlNPSmFlQVI0a1NzCjg3REM1bmdNMVhJeVFpSCtvL21zaEpUS3ZhZUVpeTVmM1BaaExJNWZNQlZwN0tWTUNZY3V2NWZ4Y3pHVHZFM1YKcHcxamJmSzRDdG9xemFmK3hrdUk5ZWNjakp4TU5KRGc0QW5CNEpxWm11Y2dQWGJPdEpRR2VHaHZqZlBqTVZHZworTHhzSUFWZE8vRjFtUUtDQVFFQTJJeFNNK1VZOTFoem5vUURSbzV4WWVGS0dUeDhVZ00rWDdycURzTXp6NUVSCkRWKzh5WlNsY29NVjNlcGVSdjFHYlRodEUvTlZ4c1k2SW5yUkVJNHB2WFJqYkxqZDZPVkJYWENsYVl1YWsyV20KV2QxTVo4dDZRMUtVWXBFS0piZVRMN09SUmtibnIzTHhmWGJ2WTRPV1BaQjZyNktoaXljbTFubUNJU0hiMFh5Mwp1WHY1VVZEYVZWdklnS0RkNGhrRGZSWmEzNEZZUDYvcUFzMzkyWkJnclpvbVk0SkFMN2F0RnpmWVVZMUtlamV3CmpJWCtpQmRkdkd0cXQ0ZzYwQkgzQUxCZjJFb0Q4bkluaHRuUWtSd0d5QnRFN1pRVGdCYzRJbm5mR2tMZTRpWDkKQlZaSFgxb0VHWUp3RkVUNk1zUHFwcU8yWDhPT21YRDFFVFhUTUVjOGx3S0NBUUFmMWQwUG1xaEcrL2orM0hObQpDdlY3OGZUZUNueHhBY3grSmY0SXV1NEx5dTdTZ0pWMGxYL200cUlHdWo5L083bk4vbnhaY0lTNVdtQm1HZGNyCmVQMFI3QXgwUHBnS3lSeGNGUmFVRnVoaU5abGVnUnZPeWQ4YXV5UXNGWUhYTWR1d3FiakFPc080UTVVTDVaY0IKRUNNQ3U4cDFObS9sKzZidk1qUHErS3BBdGtFbmhneWhLbWhwTS9GSnVPcEFIUWtud21JTUVGZE54a29jZHZjUQp2LzJEVWVjSk5yWHRFMU5pU2l4cDFyMCtQZmdpU3VvenhVODMyY21Jb1FxQ1l4SWNqUlJFZ0xWQktoVGNwU1RmCklXdkx3aEsxZUNCZHRrU1VUY1AyTTRrTTI3VkpSaWJ4TjBXTko3bFl5STVkRVByeUQ3WUpNa0hVVWxpUGVLR2gKalc1aEFvSUJBQWdWQktSbk1vMVl3Y2Z5eVdTQ3dIeVV1ZjFESXFpMDhra0VZdVAyS1NMZ0dURFVsK2sySVE2cgpFYy9jaFhSRTA3SVQzdzVWa0tnQWtmN2pjcFlabURrMzlOWUQrRlJPNmllZ29xdlR5QXNrU2hja2lVdCticXZBCmswVXlnSnh6dzR5T09TZlVVYVZjdHVLbDQ3MWxGZUJxV2duZ0dnTmxqSytJalhETElMY3EzbmlQeGZoZytpVWgKYmRSUExMalpraVhEQmRVOXNKdC81MDMvZmkvMmtZVXBNYkdaRk9neSt6YllvTHc2ZDhNai9QVGhzMlJFNnZ5egpUYUpYOVVuNndhdEc2ZXphcGxjUUo2V0N6NlA2MWMzMkpwWnZabUxyZXU3ZWVaTXpWN285RExwOFErR3RMR1gvClZrdUxYNE14aUxwN2RiMFJRV3M4cWdqZ1oyZHY0VFVDZ2dFQkFMRjRiNnhRNjJJaCtMaTdsVk9lSWQ5VFVub08KUU1LUVNRN0xlWjJ4TmhCYWRPUEt0ZmJ5U0dGMGZieXZiVWk2czAyVnJpWC93S1V6T2o1WEFUbUZYQVdzYnU1dwo2M1JVR09ua2Z6cjIwWDZJWTVzOS9kdnJWZXFLNkpLdlQyZ0F0dWMwNXNCZzJPaG5CdHh2c0JDekhYVy9YRWJsCktWamVIMUxQTnZMaFNSc3BvT2FFVUhlaHpNN2c1V3FGSXhSQmRlb2J1SWNxQ1J2WjRFZGl6b05ybzVRZXFub3oKMTlyU0VVcTNBMEdIdE5Pb0xuV2Q3ZkZta2NOMEw5S3R0MTdsK2wxV0c3Y2kxVTVuSXBlOXBxZThlUUU2YmNYaApkNnlkdWd3UUpXbUxKSlpMQUs3eFpZdzd1ODhoa3ppZ2pSR2ltWHZ4VTJCMTU5OW5OT2NrNWQ0YXJTRT0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0= +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 diff --git a/docker-compose.yml b/docker-compose.yml index 98b8426..67468ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,32 @@ services: ports: - "${DATABASE_PORT:-5432}:5432" 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 service: @@ -30,9 +55,10 @@ services: context: service depends_on: - db + - redis networks: - - weather-frontend - - weather-backend + - frontend + - backend restart: unless-stopped ui: @@ -48,13 +74,14 @@ services: depends_on: - service networks: - - weather-frontend + - frontend restart: unless-stopped volumes: db: db_logs: + minio: networks: - weather-frontend: {} - weather-backend: {} + frontend: + backend: diff --git a/service/.env.TEMPLATE b/service/.env.TEMPLATE index 69fc7e7..2391b8c 100644 --- a/service/.env.TEMPLATE +++ b/service/.env.TEMPLATE @@ -1,4 +1,6 @@ -RUST_LOG=waren,service=info +RUST_LOG=warn,service=debug + +DATABASE_CONTAINER=weather-service DATABASE_USER=weather DATABASE_PASSWORD= @@ -9,6 +11,12 @@ DATABASE_PORT=5432 REDIS_HOST=localhost 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_PORT=5000 diff --git a/service/Dockerfile b/service/Dockerfile index faab563..0b06a41 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -1,12 +1,24 @@ -FROM rust:1.72.1-bookworm - -WORKDIR /service -USER root +# ========= +# Builder +# ========= +FROM rust:bookworm as builder +WORKDIR /builder COPY migrations ./migrations 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 -CMD ["./target/release/weather-service"] \ No newline at end of file +# ========= +# 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"] diff --git a/service/Makefile b/service/Makefile index 46caff1..1506881 100644 --- a/service/Makefile +++ b/service/Makefile @@ -1,9 +1,8 @@ #!make +SHELL := /bin/bash include .env -SHELL := /bin/bash - .PHONY: help build start stop lint 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}' @echo -build: ## Build Docker containers +build: ## Build the Docker image docker compose build utils: ## Start the utils docker compose up -d db docker compose up -d redis -up: ## Start Docker containers +up: ## Start the Docker containers docker compose up -d -down: ## Stop Docker containers +down: ## Stop the Docker containers docker compose down connect: ## Connect to the Weather DB diff --git a/service/airport-codes.json b/service/airport-codes.json index 6e9feb0..5ae0fc5 100644 --- a/service/airport-codes.json +++ b/service/airport-codes.json @@ -63666,7 +63666,7 @@ "local_code": "W43" }, { - "icao": "KW45", + "icao": "KLUA", "category": "small_airport", "full_name": "Luray Caverns Airport", "point": diff --git a/service/docker-compose.yml b/service/docker-compose.yml index f766e19..b347a9c 100644 --- a/service/docker-compose.yml +++ b/service/docker-compose.yml @@ -27,6 +27,21 @@ services: networks: - backend 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: container_name: weather-service @@ -54,6 +69,7 @@ services: volumes: db: db_logs: + minio: networks: frontend: diff --git a/service/src/airports/model.rs b/service/src/airports/model.rs index 83d7544..21a2ac6 100644 --- a/service/src/airports/model.rs +++ b/service/src/airports/model.rs @@ -1,9 +1,9 @@ +use std::str::FromStr; + use crate::db; use crate::error_handler::ServiceError; use crate::db::schema::airports; -use diesel::dsl::count_star; use diesel::prelude::*; -// use log::trace; use postgis_diesel::types::*; use postgis_diesel::functions::*; use serde::{Deserialize, Serialize}; @@ -25,6 +25,79 @@ pub struct InsertAirport { pub point: Point } +#[derive(Debug)] +pub struct QueryFilters { + pub name: Option, + pub icao: Option, + pub bounds: Option>, + pub category: Option, + pub order_field: Option, + pub order_by: Option +} + +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 { + 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 { + 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)] #[diesel(table_name = airports)] pub struct QueryAirport { @@ -44,51 +117,95 @@ pub struct QueryAirport { } impl QueryAirport { - pub fn get_all(bounds: &Option>, category: &Option, filter: &Option, order_by: bool, limit: i32, page: i32) -> Result, ServiceError> { + pub fn get_all(filters: &QueryFilters, limit: i32, page: i32) -> Result, ServiceError> { let mut conn = db::connection()?; - let mut query = airports::table - .limit(limit as i64) - .into_boxed(); - query = query.filter(airports::id.gt(std::cmp::max(0, page - 1) * limit)); + let mut query = airports::table.limit(limit as i64).into_boxed(); + // Limit query to page and limit + let offset = (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)); } - if let Some(category) = category { + if let Some(category) = &filters.category { query = query.filter(airports::category.eq(category)); } - if let Some(filter) = filter { - query = query.filter(airports::icao - .ilike(format!("%{}%", filter)) - .or(airports::full_name.ilike(format!("%{}%", filter))) - ) + if let Some(icao) = &filters.icao { + if let Some(name) = &filters.name { + query = query.filter( + airports::icao.ilike(format!("%{}%", icao)).or( + airports::full_name.ilike(format!("%{}%", name)) + ) + ) + } else { + query = query.filter(airports::icao.ilike(format!("%{}%", icao))) + } } - if order_by { - query = query.order(airports::category.asc()); + + 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()), + }; + }; + } + } } let airports: Vec = query.load::(&mut conn)?; Ok(airports) } - pub fn get_count(bounds: &Option>, category: &Option, filter: &Option) -> Result { + pub fn get_count(filters: &QueryFilters) -> Result { 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)); } - if let Some(category) = category { + if let Some(category) = &filters.category { query = query.filter(airports::category.eq(category)); } - if let Some(filter) = filter { - query = query.filter(airports::icao - .ilike(format!("%{}%", filter)) - .or(airports::full_name.ilike(format!("%{}%", filter))) - ) + if let Some(icao) = &filters.icao { + if let Some(name) = &filters.name { + query = query.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); } @@ -96,29 +213,32 @@ impl QueryAirport { let mut conn = db::connection()?; let airport = airports::table.filter(airports::icao.eq(icao)).first(&mut conn)?; Ok(airport) -} + } -pub fn create(airport: InsertAirport) -> Result { + pub fn create(airport: InsertAirport) -> Result { let mut conn = db::connection()?; let airport = InsertAirport::from(airport); let airport = diesel::insert_into(airports::table) .values(airport) .get_result(&mut conn)?; Ok(airport) -} + } -pub fn update(id: i32, airport: InsertAirport) -> Result { + pub fn update(id: i32, airport: InsertAirport) -> Result { let mut conn = db::connection()?; let airport = diesel::update(airports::table) .filter(airports::id.eq(id)) .set(airport) .get_result(&mut conn)?; Ok(airport) -} + } -pub fn delete(id: i32) -> Result { + pub fn delete(id: Option) -> Result { 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) + } } -} \ No newline at end of file diff --git a/service/src/airports/routes.rs b/service/src/airports/routes.rs index 356b963..ddc17c9 100644 --- a/service/src/airports/routes.rs +++ b/service/src/airports/routes.rs @@ -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 log::{error, warn}; use postgis_diesel::types::{Polygon, Point}; @@ -6,9 +8,12 @@ use serde::{Serialize, Deserialize}; #[derive(Debug, Serialize, Deserialize)] struct GetAllParameters { - filter: Option, + name: Option, + icao: Option, bounds: Option, category: Option, + order_field: Option, + order_by: Option, limit: Option, page: Option } @@ -19,20 +24,21 @@ async fn import(auth: JwtAuth) -> HttpResponse { Ok(_) => {}, Err(err) => return ResponseError::error_response(&err) }; - db::import_data(); - HttpResponse::Ok().body({}) + let count = db::import_data(); + HttpResponse::Ok().json(Response { + data: count, + meta: None + }) } -#[derive(Serialize, Deserialize)] -pub struct AirportsResponse { - pub data: Vec, - pub meta: Metadata -} - -#[get("/airports")] +#[get("/search")] async fn get_all(req: HttpRequest) -> HttpResponse { let params = web::Query::::from_query(req.query_string()).unwrap(); - let polygon: Option> = 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) => { let bounds: Vec<&str> = b.split(",").collect(); if bounds.len() != 4 { @@ -77,12 +83,13 @@ async fn get_all(req: HttpRequest) -> HttpResponse { }, 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 }; - let filter = match ¶ms.filter { - Some(f) => Some(f.to_string()), + filters.order_field = match ¶ms.order_field { + Some(o) => Some(QueryOrderField::from_str(&o).unwrap()), None => None }; @@ -94,16 +101,16 @@ async fn get_all(req: HttpRequest) -> HttpResponse { Some(p) => p, None => 1 }; - let total = match QueryAirport::get_count(&polygon, &category, &filter) { + let total = match QueryAirport::get_count(&filters) { Ok(t) => t, Err(_) => 0 }; 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() { - Ok(a) => HttpResponse::Ok().json(AirportsResponse { + match web::block(move || QueryAirport::get_all(&filters, limit, page)).await.unwrap() { + Ok(a) => HttpResponse::Ok().json(Response { data: a, - meta: Metadata { page, limit, pages, total } + meta: Some(Metadata { page, limit, pages, total }) }), Err(err) => { error!("{}", err); @@ -112,18 +119,12 @@ async fn get_all(req: HttpRequest) -> HttpResponse { } } -#[derive(Serialize, Deserialize)] -pub struct AirportResponse { - pub data: QueryAirport, - pub meta: Metadata -} - -#[get("/airports/{icao}")] +#[get("/search/{icao}")] async fn get(icao: web::Path) -> HttpResponse { match QueryAirport::find(icao.into_inner()) { - Ok(a) => HttpResponse::Ok().json(AirportResponse { + Ok(a) => HttpResponse::Ok().json(Response { 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) => { error!("{}", err); @@ -132,7 +133,7 @@ async fn get(icao: web::Path) -> HttpResponse { } } -#[post("/airports")] +#[post("/create")] async fn create(airport: web::Json, auth: JwtAuth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, @@ -147,7 +148,7 @@ async fn create(airport: web::Json, auth: JwtAuth) -> HttpRespons } } -#[put("/airports/{icao}")] +#[put("/update/{icao}")] async fn update(icao: web::Path, airport: web::Json, auth: JwtAuth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, @@ -162,13 +163,28 @@ async fn update(icao: web::Path, airport: web::Json, auth: J } } -#[delete("/airports/{icao}")] -async fn delete(icao: web::Path, auth: JwtAuth) -> HttpResponse { +#[delete("/remove")] +async fn remove_all(auth: JwtAuth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, 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, 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(), Err(err) => { error!("{}", err); @@ -178,10 +194,13 @@ async fn delete(icao: web::Path, auth: JwtAuth) -> HttpResponse { } pub fn init_routes(config: &mut web::ServiceConfig) { - config.service(get_all); - config.service(get); - config.service(create); - config.service(update); - config.service(delete); - config.service(import); + config.service(web::scope("airports") + .service(get_all) + .service(get) + .service(create) + .service(update) + .service(remove) + .service(remove_all) + .service(import) + ); } \ No newline at end of file diff --git a/service/src/auth/routes.rs b/service/src/auth/routes.rs index 5172a23..1563df1 100644 --- a/service/src/auth/routes.rs +++ b/service/src/auth/routes.rs @@ -4,7 +4,7 @@ use actix_web::{get, post, web, HttpResponse, ResponseError, cookie::{Cookie, ti use log::error; use redis::AsyncCommands; 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}; @@ -342,7 +342,10 @@ async fn me(auth: JwtAuth) -> HttpResponse { #[get("/roles")] 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) { diff --git a/service/src/db/mod.rs b/service/src/db/mod.rs index 172542b..a8a28d9 100644 --- a/service/src/db/mod.rs +++ b/service/src/db/mod.rs @@ -59,18 +59,26 @@ pub async fn redis_async_connection() -> Result { Ok(conn) } -pub fn import_data() { +pub fn import_data() -> i32 { let path = "airport-codes.json"; debug!("Importing data from {}", path); let contents: String = std::fs::read_to_string(path).expect("Failed to read file"); let airports: Vec = serde_json::from_str(&contents).expect("JSON was not well formed."); + let mut count = 0; for airport in airports { match QueryAirport::create(airport) { - Ok(_) => {}, + Ok(_) => count += 1, Err(err) => error!("Error inserting airport; {}", err) }; } debug!("Import complete"); + return count; +} + +#[derive(Serialize, Deserialize)] +pub struct Response { + pub data: T, + pub meta: Option } #[derive(Serialize, Deserialize)] diff --git a/service/src/scheduler.rs b/service/src/scheduler.rs index 14e75cc..e6c7dc4 100644 --- a/service/src/scheduler.rs +++ b/service/src/scheduler.rs @@ -1,14 +1,14 @@ use tokio::time::{sleep, Duration}; use log::{warn, debug, trace}; -use crate::airports::QueryAirport; +use crate::airports::{QueryAirport, QueryFilters}; use crate::metars::Metar; pub fn update_airports() { tokio::spawn(async { loop { debug!("METAR update start"); - let total = match QueryAirport::get_count(&None, &None, &None) { + let total = match QueryAirport::get_count(&QueryFilters::default()) { Ok(t) => t, Err(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 mut airports: Vec = vec![]; 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) => { airports.append(&mut a) }, diff --git a/service/src/users/routes.rs b/service/src/users/routes.rs index 1984945..512c93a 100644 --- a/service/src/users/routes.rs +++ b/service/src/users/routes.rs @@ -4,7 +4,6 @@ use crate::auth::{JwtAuth, QueryUser, InsertUser}; #[get("users/favorites")] async fn get_favorites(auth: JwtAuth) -> HttpResponse { - println!("{:?}", auth); match QueryUser::get_by_email(&auth.user.email) { Ok(user) => { return HttpResponse::Ok().json(user.favorites) diff --git a/ui/src/api/airport.ts b/ui/src/api/airport.ts index 7f7d740..590569b 100644 --- a/ui/src/api/airport.ts +++ b/ui/src/api/airport.ts @@ -1,38 +1,67 @@ -import { Bounds, GetAirportResponse, GetAirportsResponse } from './airport.types'; -import { getRequest } from '.'; +import { AirportOrderField, Bounds, GetAirportResponse, GetAirportsResponse } from './airport.types'; +import { getRequest, deleteRequest } from '.'; interface GetAirportProps { icao: string; } export async function getAirport({ icao }: GetAirportProps): Promise { - const response = await getRequest(`airports/${icao}`, {}); + const response = await getRequest(`airports/search/${icao}`); return response?.json() || { data: undefined }; } interface GetAirportsProps { bounds?: Bounds; category?: string; - filter?: string; + name?: string; + order_field?: AirportOrderField; + order_by?: 'asc' | 'desc'; + icao?: string; page?: number; limit?: number; } +export async function getAirportsCount() { + const response = await getRequest('airports/count'); + return response?.json() || { data: 0 }; +} + export async function getAirports({ bounds, category, - filter, + name, + icao, + order_field, + order_by, limit = 10, page = 1 }: GetAirportsProps): Promise { - const response = await getRequest('airports', { + const response = await getRequest('airports/search', { bounds: bounds ? `${bounds?.northEast.lat},${bounds?.northEast.lon},${bounds?.southWest.lat},${bounds?.southWest.lon}` : undefined, category: category ?? undefined, - filter: filter ?? undefined, + name: name ?? undefined, + icao: icao ?? undefined, + order_field: order_field ?? undefined, + order_by: order_by ?? undefined, limit, page }); return response?.json() || { data: [] }; } + +export async function removeAirport({ icao }: { icao?: string }): Promise { + 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 { + const response = await getRequest('airports/import'); + return response?.json() || { data: undefined }; +} diff --git a/ui/src/api/airport.types.ts b/ui/src/api/airport.types.ts index 9339c31..4461edd 100644 --- a/ui/src/api/airport.types.ts +++ b/ui/src/api/airport.types.ts @@ -1,3 +1,4 @@ +import { Metadata } from '.'; import { Metar } from './metar.types'; export enum AirportCategory { @@ -6,6 +7,19 @@ export enum AirportCategory { 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 { northEast: Coordinate; southWest: Coordinate; @@ -20,7 +34,7 @@ export interface Airport { icao: string; category: AirportCategory; full_name: string; - elevation_ft: string; + elevation_ft: number; continent: string; iso_country: string; iso_region: string; @@ -38,8 +52,10 @@ export interface Airport { export interface GetAirportResponse { data: Airport; + meta: Metadata; } export interface GetAirportsResponse { data: Airport[]; + meta: Metadata; } diff --git a/ui/src/app/admin/page.tsx b/ui/src/app/admin/page.tsx new file mode 100644 index 0000000..65f4720 --- /dev/null +++ b/ui/src/app/admin/page.tsx @@ -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(undefined); + + useEffect(() => { + console.log(airport); + }, [airport]); + + return + + + + + + + + + ; +} + + diff --git a/ui/src/components/Admin/AirportTablePanel.tsx b/ui/src/components/Admin/AirportTablePanel.tsx new file mode 100644 index 0000000..b797b08 --- /dev/null +++ b/ui/src/components/Admin/AirportTablePanel.tsx @@ -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([]); + + 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) => ( + { + console.log('here'); + setAirport(airport); + }} + style={{ cursor: 'pointer' }} + > + {airport.icao} + {airport.full_name} + {airport.category} + {airport.continent} + {airport.iso_country} + {airport.iso_region} + {airport.municipality} + {airport.gps_code} + {airport.iata_code} + {airport.local_code} + {airport.point.x} + {airport.point.y} + + )) + + return + } + value={search} + onChange={handleSearchChange} + /> + + + + + ICAO + Full Name + Category + Continent + ISO Country + ISO Region + Municipality + GPS Code + IATA Code + Local Code + Latitude + Longitude + + + {rows} +
+
+ + + { + await importAirports(); + await getAirportData(); + }}> + Import + + { + await removeAirport({}); + await getAirportData(); + }}> + Remove All + + +
+} + +function PanelButton({ children, color = 'blue', onClick }: {children: any, color?: string, onClick: () => Promise }) { + const [loading, setLoading] = useState(false); + return +} \ No newline at end of file diff --git a/ui/src/components/Admin/CreateAirportPanel.tsx b/ui/src/components/Admin/CreateAirportPanel.tsx new file mode 100644 index 0000000..b64810c --- /dev/null +++ b/ui/src/components/Admin/CreateAirportPanel.tsx @@ -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({ + 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 + Create Airport +
{ + if (airport) { + console.log('update'); + } else { + console.log('create'); + } + })}> + +