From 06c0008b9a2a52bfb4b20a96d3a3bc0bfde904de Mon Sep 17 00:00:00 2001 From: Jakub Kermes <114694305+JakubKermes@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:33:37 +0200 Subject: [PATCH 1/3] #213 - create api endpoints (#215) * Create api endpoint and user validation needed to fix user roles * Complete Api endpoints * fix ProviderController.php * fix controllers * cs * Add responses, small fixes * Added OpenAPI documentation and responses * typing * Update config/scramble.php Co-authored-by: Kamil Piech * typing * add translation --------- Co-authored-by: Kamil Piech --- api.json | 1 + app/Console/Kernel.php | 1 + .../Admin/CityAlternativeNameController.php | 27 +++ .../Controllers/Api/Admin/CityController.php | 67 +++++++ .../Api/Admin/CountryController.php | 49 +++++ .../Api/Admin/ImportInfoController.php | 32 +++ .../Api/Admin/ProviderController.php | 94 +++++++++ app/Http/Controllers/Api/AuthController.php | 116 +++++++++++ .../Api/ChangeLocaleController.php | 27 +++ .../Controllers/Api/CityOpinionController.php | 45 +++++ .../Controllers/Api/CityPageController.php | 36 ++++ .../Api/CityProviderController.php | 65 ++++++ .../CityWithoutAssignedCountryController.php | 30 +++ app/Http/Controllers/Api/Controller.php | 15 ++ .../Controllers/Api/DashboardController.php | 55 +++++ .../Controllers/Api/FavoritesController.php | 63 ++++++ app/Http/Controllers/FavoritesController.php | 12 +- app/Http/Kernel.php | 6 + app/Providers/AppServiceProvider.php | 3 + composer.json | 5 +- composer.lock | 189 +++++++++++++++++- config/auth.php | 4 + config/sanctum.php | 2 +- config/scramble.php | 27 +++ lang/pl.json | 6 + routes/api.php | 53 ++++- 26 files changed, 1016 insertions(+), 14 deletions(-) create mode 100644 api.json create mode 100644 app/Http/Controllers/Api/Admin/CityAlternativeNameController.php create mode 100644 app/Http/Controllers/Api/Admin/CityController.php create mode 100644 app/Http/Controllers/Api/Admin/CountryController.php create mode 100644 app/Http/Controllers/Api/Admin/ImportInfoController.php create mode 100644 app/Http/Controllers/Api/Admin/ProviderController.php create mode 100644 app/Http/Controllers/Api/AuthController.php create mode 100644 app/Http/Controllers/Api/ChangeLocaleController.php create mode 100644 app/Http/Controllers/Api/CityOpinionController.php create mode 100644 app/Http/Controllers/Api/CityPageController.php create mode 100644 app/Http/Controllers/Api/CityProviderController.php create mode 100644 app/Http/Controllers/Api/CityWithoutAssignedCountryController.php create mode 100644 app/Http/Controllers/Api/Controller.php create mode 100644 app/Http/Controllers/Api/DashboardController.php create mode 100644 app/Http/Controllers/Api/FavoritesController.php create mode 100644 config/scramble.php diff --git a/api.json b/api.json new file mode 100644 index 00000000..0ba4fdfa --- /dev/null +++ b/api.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"Escooters","version":"0.0.1"},"servers":[{"url":"http:\/\/escooters.blumilk.localhost\/api"}],"paths":{"\/register":{"post":{"operationId":"register","tags":["Auth"],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string","format":"email"},"password":{"type":"string"}},"required":["name","email","password"]}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"422":{"$ref":"#\/components\/responses\/ValidationException"},"403":{"$ref":"#\/components\/responses\/AuthorizationException"}}}},"\/login":{"post":{"operationId":"login","tags":["Auth"],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"password":{"type":"string"}},"required":["email","password"]}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"anyOf":[{"type":"string"},{"type":"object","properties":{"":{"type":"array","items":{"type":"string"}},"access_token":{"type":"string"}},"required":[null,"access_token"]}]}}}},"422":{"$ref":"#\/components\/responses\/ValidationException"},"403":{"$ref":"#\/components\/responses\/AuthorizationException"}}}},"\/logout":{"post":{"operationId":"logout","tags":["Auth"],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"\/language\/{locale}":{"post":{"operationId":"changeLocale","tags":["ChangeLocale"],"parameters":[{"name":"locale","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"\/admin\/cities":{"get":{"operationId":"cities.index","tags":["City"],"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"cities":{"type":"array","items":{"$ref":"#\/components\/schemas\/CityResource"}},"providers":{"type":"array","items":{"$ref":"#\/components\/schemas\/ProviderResource"}},"countries":{"type":"array","items":{"$ref":"#\/components\/schemas\/CountryResource"}},"citiesWithoutAssignedCountry":{"type":"array","items":{"$ref":"#\/components\/schemas\/CityWithoutAssignedCountryResource"}}},"required":["cities","providers","countries","citiesWithoutAssignedCountry"]}}}}}},"post":{"operationId":"cities.store","tags":["City"],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"latitude":{"type":"number"},"longitude":{"type":"number"},"country_id":{"type":"integer"}},"required":["name","latitude","longitude"]}}}},"responses":{"201":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"422":{"$ref":"#\/components\/responses\/ValidationException"},"403":{"$ref":"#\/components\/responses\/AuthorizationException"}}}},"\/admin\/cities\/create":{"get":{"operationId":"cities.create","tags":["City"]}},"\/admin\/cities\/{city}":{"get":{"operationId":"cities.show","tags":["City"],"parameters":[{"name":"city","in":"path","required":true,"schema":{"type":"string"}}]},"put":{"operationId":"cities.update","tags":["City"],"parameters":[{"name":"city","in":"path","required":true,"description":"The city ID","schema":{"type":"integer"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"latitude":{"type":"number"},"longitude":{"type":"number"},"country_id":{"type":"integer"}},"required":["name","latitude","longitude"]}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"},"422":{"$ref":"#\/components\/responses\/ValidationException"},"403":{"$ref":"#\/components\/responses\/AuthorizationException"}}},"delete":{"operationId":"cities.destroy","tags":["City"],"parameters":[{"name":"city","in":"path","required":true,"description":"The city ID","schema":{"type":"integer"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"}}}},"\/admin\/cities\/{city}\/edit":{"get":{"operationId":"cities.edit","tags":["City"],"parameters":[{"name":"city","in":"path","required":true,"schema":{"type":"string"}}]}},"\/city-alternative-name":{"get":{"operationId":"city-alternative-name.index","tags":["CityAlternativeName"]},"post":{"operationId":"city-alternative-name.store","tags":["CityAlternativeName"],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"city_id":{"type":"integer"}},"required":["name"]}}}},"responses":{"201":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"422":{"$ref":"#\/components\/responses\/ValidationException"},"403":{"$ref":"#\/components\/responses\/AuthorizationException"}}}},"\/city-alternative-name\/create":{"get":{"operationId":"city-alternative-name.create","tags":["CityAlternativeName"]}},"\/city-alternative-name\/{city_alternative_name}":{"get":{"operationId":"city-alternative-name.show","tags":["CityAlternativeName"],"parameters":[{"name":"city_alternative_name","in":"path","required":true,"schema":{"type":"string"}}]},"put":{"operationId":"city-alternative-name.update","tags":["CityAlternativeName"],"parameters":[{"name":"city_alternative_name","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}}}},"\/city-alternative-name\/{city_alternative_name}\/edit":{"get":{"operationId":"city-alternative-name.edit","tags":["CityAlternativeName"],"parameters":[{"name":"city_alternative_name","in":"path","required":true,"schema":{"type":"string"}}]}},"\/city-alternative-name\/{cityAlternativeName}":{"delete":{"operationId":"city-alternative-name.destroy","tags":["CityAlternativeName"],"parameters":[{"name":"cityAlternativeName","in":"path","required":true,"description":"The city alternative name ID","schema":{"type":"integer"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"}}}},"\/opinions":{"post":{"operationId":"cityOpinion.store","tags":["CityOpinion"],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"rating":{"type":"number","minimum":1,"maximum":5},"content":{"type":"string"},"city_id":{"type":"number"}},"required":["rating","content","city_id"]}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"422":{"$ref":"#\/components\/responses\/ValidationException"},"403":{"$ref":"#\/components\/responses\/AuthorizationException"}}}},"\/opinions\/{cityOpinion}":{"patch":{"operationId":"cityOpinion.update","tags":["CityOpinion"],"parameters":[{"name":"cityOpinion","in":"path","required":true,"description":"The city opinion ID","schema":{"type":"integer"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"rating":{"type":"number","minimum":1,"maximum":5},"content":{"type":"string"},"city_id":{"type":"number"}},"required":["rating","content","city_id"]}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"},"422":{"$ref":"#\/components\/responses\/ValidationException"},"403":{"$ref":"#\/components\/responses\/AuthorizationException"}}},"delete":{"operationId":"cityOpinion.destroy","tags":["CityOpinion"],"parameters":[{"name":"cityOpinion","in":"path","required":true,"description":"The city opinion ID","schema":{"type":"integer"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"}}}},"\/{country}\/{city}":{"get":{"operationId":"cityPage.index","tags":["CityPage"],"parameters":[{"name":"country","in":"path","required":true,"description":"The country slug","schema":{"type":"string"}},{"name":"city","in":"path","required":true,"description":"The city slug","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"city":{"$ref":"#\/components\/schemas\/CityResource"},"providers":{"type":"array","items":{"$ref":"#\/components\/schemas\/ProviderResource"}},"cityOpinions":{"type":"array","items":{"$ref":"#\/components\/schemas\/CityOpinionResource"}}},"required":["city","providers","cityOpinions"]}}}},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"}}}},"\/providers":{"get":{"operationId":"cityProvider.index","tags":["CityProvider"],"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"cities":{"type":"array","items":{"$ref":"#\/components\/schemas\/CityResource"}},"providers":{"type":"array","items":{"$ref":"#\/components\/schemas\/ProviderResource"}},"countries":{"type":"array","items":{"$ref":"#\/components\/schemas\/CountryResource"}}},"required":["cities","providers","countries"]}}}}}}},"\/update-city-providers\/{city}":{"patch":{"operationId":"cityProvider.update","tags":["CityProvider"],"parameters":[{"name":"city","in":"path","required":true,"description":"The city ID","schema":{"type":"integer"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"providerNames":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"},"422":{"$ref":"#\/components\/responses\/ValidationException"},"403":{"$ref":"#\/components\/responses\/AuthorizationException"}}}},"\/run-importers":{"post":{"operationId":"cityProvider.runImporters","tags":["CityProvider"],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"\/delete-city-without-assigned-country\/{city}":{"delete":{"operationId":"cityWithoutAssignedCountry.destroy","tags":["CityWithoutAssignedCountry"],"parameters":[{"name":"city","in":"path","required":true,"description":"The city ID","schema":{"type":"integer"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"}}}},"\/delete-all-cities-without-assigned-country":{"post":{"operationId":"cityWithoutAssignedCountry.destroyAll","tags":["CityWithoutAssignedCountry"],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"\/admin\/countries":{"get":{"operationId":"countries.index","tags":["Country"],"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"countries":{"type":"array","items":{"$ref":"#\/components\/schemas\/CountryResource"}}},"required":["countries"]}}}}}},"post":{"operationId":"countries.store","tags":["Country"],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"alternative_name":{"type":["string","null"]},"latitude":{"type":"number"},"longitude":{"type":"number"},"iso":{"type":"string"}},"required":["name","latitude","longitude","iso"]}}}},"responses":{"201":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"422":{"$ref":"#\/components\/responses\/ValidationException"},"403":{"$ref":"#\/components\/responses\/AuthorizationException"}}}},"\/admin\/countries\/create":{"get":{"operationId":"countries.create","tags":["Country"]}},"\/admin\/countries\/{country}":{"get":{"operationId":"countries.show","tags":["Country"],"parameters":[{"name":"country","in":"path","required":true,"schema":{"type":"string"}}]},"put":{"operationId":"countries.update","tags":["Country"],"parameters":[{"name":"country","in":"path","required":true,"description":"The country ID","schema":{"type":"integer"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"alternative_name":{"type":["string","null"]},"latitude":{"type":"number"},"longitude":{"type":"number"},"iso":{"type":"string"}},"required":["name","latitude","longitude","iso"]}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"},"422":{"$ref":"#\/components\/responses\/ValidationException"},"403":{"$ref":"#\/components\/responses\/AuthorizationException"}}},"delete":{"operationId":"countries.destroy","tags":["Country"],"parameters":[{"name":"country","in":"path","required":true,"description":"The country ID","schema":{"type":"integer"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"}}}},"\/admin\/countries\/{country}\/edit":{"get":{"operationId":"countries.edit","tags":["Country"],"parameters":[{"name":"country","in":"path","required":true,"schema":{"type":"string"}}]}},"\/admin\/dashboard":{"get":{"operationId":"dashboard.index","tags":["Dashboard"],"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"usersCount":{"type":"string"},"citiesWithProvidersCount":{"type":"string"},"countriesWithCitiesWithProvidersCount":{"type":"string"},"providersCount":{"type":"string"},"providerCitiesCount":{"type":"string"},"providers":{"type":"array","items":{"$ref":"#\/components\/schemas\/ProviderResource"}}},"required":["usersCount","citiesWithProvidersCount","countriesWithCitiesWithProvidersCount","providersCount","providerCitiesCount","providers"]}}}}}},"post":{"operationId":"dashboard.store","tags":["Dashboard"],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}}}},"\/admin\/dashboard\/create":{"get":{"operationId":"dashboard.create","tags":["Dashboard"]}},"\/admin\/dashboard\/{dashboard}":{"get":{"operationId":"dashboard.show","tags":["Dashboard"],"parameters":[{"name":"dashboard","in":"path","required":true,"schema":{"type":"string"}}]},"put":{"operationId":"dashboard.update","tags":["Dashboard"],"parameters":[{"name":"dashboard","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}}},"delete":{"operationId":"dashboard.destroy","tags":["Dashboard"],"parameters":[{"name":"dashboard","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}}}},"\/admin\/dashboard\/{dashboard}\/edit":{"get":{"operationId":"dashboard.edit","tags":["Dashboard"],"parameters":[{"name":"dashboard","in":"path","required":true,"schema":{"type":"string"}}]}},"\/favorites":{"post":{"operationId":"favorites.store","tags":["Favorites"],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"anyOf":[{"type":"object","properties":{"message":{"type":"string","example":"City removed from favorites."}},"required":["message"]},{"type":"object","properties":{"message":{"type":"string","example":"City added to favorites."}},"required":["message"]}]}}}}}}},"\/favorites\/{cityId}":{"get":{"operationId":"favorites.check","tags":["Favorites"],"parameters":[{"name":"cityId","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"boolean"}}}}}}},"\/favorite-cities":{"get":{"operationId":"favorites.index","tags":["Favorites"],"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"cities":{"type":"string"},"providers":{"type":"array","items":{"$ref":"#\/components\/schemas\/ProviderResource"}}},"required":["cities","providers"]}}}}}}},"\/admin\/importers":{"get":{"operationId":"importInfo.index","tags":["ImportInfo"],"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"importInfo":{"type":"array","items":{"$ref":"#\/components\/schemas\/ImportInfoResource"}},"codes":{"type":"string"},"providers":{"type":"string"}},"required":["importInfo","codes","providers"]}}}}}}},"\/admin\/providers":{"get":{"operationId":"providers.index","tags":["Provider"],"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"providers":{"type":"array","items":{"$ref":"#\/components\/schemas\/ProviderResource"}}},"required":["providers"]}}}}}},"post":{"operationId":"providers.store","tags":["Provider"],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"color":{"type":"string"},"file":{"type":"string"}},"required":["name","color","file"]}}}},"responses":{"201":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"422":{"$ref":"#\/components\/responses\/ValidationException"},"403":{"$ref":"#\/components\/responses\/AuthorizationException"}}}},"\/admin\/providers\/create":{"get":{"operationId":"providers.create","tags":["Provider"]}},"\/admin\/providers\/{provider}":{"get":{"operationId":"providers.show","tags":["Provider"],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string"}}]},"put":{"operationId":"providers.update","tags":["Provider"],"parameters":[{"name":"provider","in":"path","required":true,"description":"The provider name","schema":{"type":"string"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"color":{"type":"string"},"file":{"type":"string"}},"required":["name","color","file"]}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"},"422":{"$ref":"#\/components\/responses\/ValidationException"},"403":{"$ref":"#\/components\/responses\/AuthorizationException"}}},"delete":{"operationId":"providers.destroy","tags":["Provider"],"parameters":[{"name":"provider","in":"path","required":true,"description":"The provider name","schema":{"type":"string"}}],"requestBody":{"content":{"application\/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"404":{"$ref":"#\/components\/responses\/ModelNotFoundException"}}}},"\/admin\/providers\/{provider}\/edit":{"get":{"operationId":"providers.edit","tags":["Provider"],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string"}}]}},"\/images\/providers\/{filename}":{"get":{"operationId":"provider.showLogo","tags":["Provider"],"parameters":[{"name":"filename","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application\/json":{"schema":{"type":"object","properties":{"image":{"type":"string"},"mime_type":{"type":"string","example":"image\/png"}},"required":["image","mime_type"]}}}}}}}},"components":{"schemas":{"CityAlternativeName":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"city_id":{"type":"integer"},"created_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":["string","null"],"format":"date-time"}},"required":["id","name","city_id","created_at","updated_at"],"title":"CityAlternativeName"},"CityOpinion":{"type":"object","properties":{"id":{"type":"integer"},"content":{"type":"string"},"rating":{"type":"integer"},"city_id":{"type":"integer"},"user_id":{"type":"integer"},"created_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":["string","null"],"format":"date-time"}},"required":["id","content","rating","city_id","user_id","created_at","updated_at"],"title":"CityOpinion"},"CityOpinionResource":{"type":"object","properties":{"id":{"type":"integer"},"rating":{"type":"integer"},"content":{"type":"string"},"updated_at":{"type":["string","null"],"format":"date-time"},"user":{"$ref":"#\/components\/schemas\/User"}},"required":["id","rating","content","updated_at","user"],"title":"CityOpinionResource"},"CityProvider":{"type":"object","properties":{"id":{"type":"integer"},"provider_name":{"type":"string"},"city_id":{"type":"integer"},"created_by":{"type":["string","null"]},"created_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":["string","null"],"format":"date-time"}},"required":["id","provider_name","city_id","created_by","created_at","updated_at"],"title":"CityProvider"},"CityResource":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"slug":{"type":"string"},"latitude":{"type":["string","null"]},"longitude":{"type":["string","null"]},"city_alternative_names":{"type":"array","items":{"$ref":"#\/components\/schemas\/CityAlternativeName"}},"cityProviders":{"type":"array","items":{"$ref":"#\/components\/schemas\/CityProvider"}},"country":{"$ref":"#\/components\/schemas\/Country"},"cityOpinions":{"type":"array","items":{"$ref":"#\/components\/schemas\/CityOpinion"}}},"required":["id","name","slug","latitude","longitude","city_alternative_names","cityProviders","country","cityOpinions"],"title":"CityResource"},"CityWithoutAssignedCountryResource":{"type":"object","properties":{"id":{"type":"integer"},"city_name":{"type":"string"},"country_name":{"type":"string"}},"required":["id","city_name","country_name"],"title":"CityWithoutAssignedCountryResource"},"Country":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"slug":{"type":"string"},"alternative_name":{"type":["string","null"]},"latitude":{"type":["string","null"]},"longitude":{"type":["string","null"]},"iso":{"type":"string"},"created_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":["string","null"],"format":"date-time"}},"required":["id","name","slug","alternative_name","latitude","longitude","iso","created_at","updated_at"],"title":"Country"},"CountryResource":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"slug":{"type":"string"},"alternative_name":{"type":["string","null"]},"latitude":{"type":["string","null"]},"longitude":{"type":["string","null"]},"iso":{"type":"string"}},"required":["id","name","slug","alternative_name","latitude","longitude","iso"],"title":"CountryResource"},"ImportInfoResource":{"type":"string","title":"ImportInfoResource"},"ProviderResource":{"type":"object","properties":{"name":{"type":"string"},"url":{"type":["string","null"]},"android_url":{"type":["string","null"]},"ios_url":{"type":["string","null"]},"color":{"type":"string"}},"required":["name","url","android_url","ios_url","color"],"title":"ProviderResource"},"User":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"email":{"type":"string"},"email_verified_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":["string","null"],"format":"date-time"}},"required":["id","name","email","email_verified_at","created_at","updated_at"],"title":"User"}},"responses":{"ValidationException":{"description":"Validation error","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Errors overview."},"errors":{"type":"object","description":"A detailed description of each field that failed validation.","additionalProperties":{"type":"array","items":{"type":"string"}}}},"required":["message","errors"]}}}},"AuthorizationException":{"description":"Authorization error","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Error overview."}},"required":["message"]}}}},"ModelNotFoundException":{"description":"Not found","content":{"application\/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Error overview."}},"required":["message"]}}}}}}} \ No newline at end of file diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 3e4d6ff7..bfe40033 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -16,6 +16,7 @@ protected function schedule(Schedule $schedule): void $service = new DataImporterService(); $service->run("server"); })->monthly(); + $schedule->command("sanctum:prune-expired --hours=24")->daily(); } protected function commands(): void diff --git a/app/Http/Controllers/Api/Admin/CityAlternativeNameController.php b/app/Http/Controllers/Api/Admin/CityAlternativeNameController.php new file mode 100644 index 00000000..2f119649 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/CityAlternativeNameController.php @@ -0,0 +1,27 @@ +create($request->validated()); + + return response()->json(["message" => __("City alternative name created successfully.")], 201); + } + + public function destroy(CityAlternativeName $cityAlternativeName): JsonResponse + { + $cityAlternativeName->delete(); + + return response()->json(["message" => __("City alternative name deleted successfully.")]); + } +} diff --git a/app/Http/Controllers/Api/Admin/CityController.php b/app/Http/Controllers/Api/Admin/CityController.php new file mode 100644 index 00000000..d12cf59f --- /dev/null +++ b/app/Http/Controllers/Api/Admin/CityController.php @@ -0,0 +1,67 @@ +with("cityAlternativeNames", "cityProviders", "country") + ->orderByProvidersCount() + ->searchCityNames() + ->orderByName() + ->orderByCountry() + ->orderByTimeRange() + ->orderByEmptyCoordinates() + ->paginate(15) + ->withQueryString(); + + $providers = Provider::all(); + $countries = Country::all(); + + $citiesWithoutAssignedCountry = CityWithoutAssignedCountry::all(); + + return response()->json([ + "cities" => CityResource::collection($cities), + "providers" => ProviderResource::collection($providers), + "countries" => CountryResource::collection($countries), + "citiesWithoutAssignedCountry" => CityWithoutAssignedCountryResource::collection($citiesWithoutAssignedCountry), + ]); + } + + public function store(CityRequest $request): JsonResponse + { + City::query()->create($request->validated()); + + return response()->json(["message" => __("City created successfully.")], 201); + } + + public function update(CityRequest $request, City $city): JsonResponse + { + $city->update($request->validated()); + + return response()->json(["message" => __("City updated successfully.")]); + } + + public function destroy(City $city): JsonResponse + { + $city->delete(); + + return response()->json(["message" => __("City deleted successfully.")]); + } +} diff --git a/app/Http/Controllers/Api/Admin/CountryController.php b/app/Http/Controllers/Api/Admin/CountryController.php new file mode 100644 index 00000000..3b8246c9 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/CountryController.php @@ -0,0 +1,49 @@ +search("name") + ->orderByName() + ->orderByTimeRange() + ->paginate(15) + ->withQueryString(); + + return response()->json([ + "countries" => CountryResource::collection($countries), + ]); + } + + public function store(CountryRequest $request): JsonResponse + { + Country::query()->create($request->validated()); + + return response()->json(["message" => __("Country created successfully.")], 201); + } + + public function update(CountryRequest $request, Country $country): JsonResponse + { + $country->update($request->validated()); + + return response()->json(["message" => __("Country updated successfully.")]); + } + + public function destroy(Country $country): JsonResponse + { + $country->delete(); + + return response()->json(["message" => __("Country deleted successfully.")]); + } +} diff --git a/app/Http/Controllers/Api/Admin/ImportInfoController.php b/app/Http/Controllers/Api/Admin/ImportInfoController.php new file mode 100644 index 00000000..166cac0a --- /dev/null +++ b/app/Http/Controllers/Api/Admin/ImportInfoController.php @@ -0,0 +1,32 @@ +with("importInfoDetails") + ->orderByDesc("created_at") + ->paginate(15) + ->withQueryString(); + $codes = Code::all(); + $providers = Provider::all(); + + return response()->json([ + "importInfo" => ImportInfoResource::collection($importInfo), + "codes" => $codes, + "providers" => $providers, + ]); + } +} diff --git a/app/Http/Controllers/Api/Admin/ProviderController.php b/app/Http/Controllers/Api/Admin/ProviderController.php new file mode 100644 index 00000000..7f3f59a9 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/ProviderController.php @@ -0,0 +1,94 @@ +search("name") + ->orderByName() + ->orderByTimeRange() + ->paginate(self::ITEMS_PER_PAGE) + ->withQueryString(); + + return response()->json([ + "providers" => ProviderResource::collection($providers), + ]); + } + + public function store(ProviderRequest $request): JsonResponse + { + Provider::query()->create($request->validated()); + + $fileName = $this->getFilename($request->name, $request->file("file")); + $fileContents = $request->file("file")->get(); + + Storage::disk("public")->put("providers/" . $fileName, $fileContents); + + return response()->json(["message" => __("Provider created successfully.")], 201); + } + + public function update(ProviderRequest $request, Provider $provider): JsonResponse + { + $provider->update($request->validated()); + + $imageName = $this->getFilename($request->name, $request->file("file")); + $storageImagePath = storage_path("app/public/providers/" . $imageName); + $resourceImagePath = resource_path("providers/" . $imageName); + $imageContents = $request->file("file")->get(); + + if (file_exists($resourceImagePath)) { + file_put_contents($resourceImagePath, $imageContents); + Storage::put($storageImagePath, file_get_contents($imageContents)); + } else { + Storage::put($storageImagePath, file_get_contents($imageContents)); + } + + return response()->json(["message" => __("Provider updated successfully.")]); + } + + public function destroy(Provider $provider): JsonResponse + { + $provider->delete(); + $imagePath = storage_path("app/public/providers/" . strtolower($provider["name"]) . ".png"); + File::delete($imagePath); + + return response()->json(["message" => __("Provider deleted successfully.")]); + } + + public function showLogo(string $filename): JsonResponse + { + $imagePath = storage_path("app/public/providers/" . $filename); + + if (!file_exists($imagePath)) { + $imagePath = storage_path("app/public/providers/unknown.png"); + } + + $imageData = base64_encode(file_get_contents($imagePath)); + + return response()->json([ + "image" => $imageData, + "mime_type" => "image/png", + ]); + } + + private function getFilename(string $name, UploadedFile $file): string + { + return strtolower($name) . "." . $file->getClientOriginalExtension(); + } +} diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php new file mode 100644 index 00000000..1225cc83 --- /dev/null +++ b/app/Http/Controllers/Api/AuthController.php @@ -0,0 +1,116 @@ + $request->input("name"), + "email" => $request->input("email"), + "password" => Hash::make($request->input("password")), + ]); + + return response()->json([ + "message" => __("User created."), + ]); + } + + public function login(LoginRequest $request): JsonResponse + { + $remember = $request->boolean("remember", false); + + if (Auth::attempt([ + "email" => $request->email, + "password" => $request->password, + ], $remember)) { + $user = Auth::user(); + $user_id = (string)Auth::id(); + + $token_abilities = $this->getUserAbilities($user); + + $token = $user->createToken($user_id, $token_abilities)->plainTextToken; + + return response()->json([ + $token_abilities, + "access_token" => $token, + ]); + } + + return response()->json([ + "message" => __("Invalid credentials."), + ], Response::HTTP_UNAUTHORIZED); + } + + public function logout(Request $request): JsonResponse + { + $request->user()->currentAccessToken()->delete(); + + return response()->json([ + "message" => __("Logged out."), + ]); + } + + public function redirectToProvider(string $provider): JsonResponse + { + $redirect_url = Socialite::driver($provider)->stateless()->redirect()->getTargetUrl(); + + return response()->json([ + "redirect_url" => $redirect_url, + ]); + } + + public function handleProviderRedirect(string $provider): JsonResponse + { + try { + $user = Socialite::driver($provider)->user(); + + $user = User::firstOrCreate([ + "email" => $user->getEmail(), + ], [ + "name" => $user->getName(), + "password" => Hash::make(Str::password(8)), + ]); + $token_abilities = $this->getUserAbilities($user); + + $user_id = $user->id->toString(); + $token = $user->createToken($user_id, $token_abilities)->plainTextToken; + + return response()->json([ + "access_token" => $token, + ]); + } catch (Exception $e) { + return response()->json([ + "message" => __("Login failed."), + ]); + } + } + + private function getUserAbilities(Authenticatable $user): array + { + $abilities = []; + + if ($user->isAdmin()) { + $abilities[] = "HasAdminRole"; + } + + return $abilities; + } +} diff --git a/app/Http/Controllers/Api/ChangeLocaleController.php b/app/Http/Controllers/Api/ChangeLocaleController.php new file mode 100644 index 00000000..408970cb --- /dev/null +++ b/app/Http/Controllers/Api/ChangeLocaleController.php @@ -0,0 +1,27 @@ +setLocale($locale); + + return response()->json([ + "message" => __("Language has been changed."), + ]); + } + + return response()->json([ + "message" => __("Error changing the language."), + ]); + } +} diff --git a/app/Http/Controllers/Api/CityOpinionController.php b/app/Http/Controllers/Api/CityOpinionController.php new file mode 100644 index 00000000..7565e043 --- /dev/null +++ b/app/Http/Controllers/Api/CityOpinionController.php @@ -0,0 +1,45 @@ +user() + ->cityOpinions() + ->create($request->validated()); + + return response()->json([ + "message" => __("Opinion added successfully."), + ]); + } + + public function update(CityOpinionRequest $request, CityOpinion $cityOpinion): JsonResponse + { + $opinion = $request->validated(); + $opinion["user_id"] = $request->user()->id; + + $cityOpinion->update($opinion); + + return response()->json([ + "message" => __("Opinion edited successfully."), + ]); + } + + public function destroy(CityOpinion $cityOpinion): JsonResponse + { + $cityOpinion->delete(); + + return response()->json([ + "message" => __("Opinion removed successfully!"), + ]); + } +} diff --git a/app/Http/Controllers/Api/CityPageController.php b/app/Http/Controllers/Api/CityPageController.php new file mode 100644 index 00000000..5e35f2e9 --- /dev/null +++ b/app/Http/Controllers/Api/CityPageController.php @@ -0,0 +1,36 @@ +whereBelongsTo($country) + ->where("id", $city->id) + ->with("cityProviders", "country") + ->firstOrFail(); + + $providers = Provider::all(); + + $cityOpinions = $selectedCity->cityOpinions()->with(["user"])->orderByDesc("updated_at")->paginate("4")->withQueryString(); + + return response()->json([ + "city" => new CityResource($selectedCity), + "providers" => ProviderResource::collection($providers), + "cityOpinions" => CityOpinionResource::collection($cityOpinions), + ]); + } +} diff --git a/app/Http/Controllers/Api/CityProviderController.php b/app/Http/Controllers/Api/CityProviderController.php new file mode 100644 index 00000000..d4990c6a --- /dev/null +++ b/app/Http/Controllers/Api/CityProviderController.php @@ -0,0 +1,65 @@ +has("cityProviders") + ->whereHas("cityProviders", fn($query): Builder => $query->whereNotNull("latitude")->whereNotNull("longitude")) + ->get() + ->sortBy("name") + ->sortByDesc(fn(City $city): int => $city->cityProviders->count()), + ); + + $providers = ProviderResource::collection(Provider::all()->sortBy("name")); + $countries = Country::whereHas("cities.cityProviders") + ->with(["cities.cityAlternativeNames", "cities.cityProviders"]) + ->get() + ->sortBy("name"); + + $countries = CountryResource::collection($countries); + + return response()->json([ + "cities" => $cities, + "providers" => $providers, + "countries" => $countries, + ]); + } + + public function update(CityProviderService $service, CityProviderRequest $request, City $city): JsonResponse + { + $service->updateProvider($request->providerNames, $city); + + return response()->json([ + "message" => __("City providers updated successfully."), + ]); + } + + public function runImporters(DataImporterService $service): JsonResponse + { + $service->run(); + + return response()->json([ + "message" => __("Importers started."), + ]); + } +} diff --git a/app/Http/Controllers/Api/CityWithoutAssignedCountryController.php b/app/Http/Controllers/Api/CityWithoutAssignedCountryController.php new file mode 100644 index 00000000..a3068ecd --- /dev/null +++ b/app/Http/Controllers/Api/CityWithoutAssignedCountryController.php @@ -0,0 +1,30 @@ +delete(); + + return response()->json([ + "message" => __("City removed successfully!"), + ]); + } + + public function destroyAll(): JsonResponse + { + CityWithoutAssignedCountry::query()->delete(); + + return response()->json([ + "message" => __("All cities removed successfully!"), + ]); + } +} diff --git a/app/Http/Controllers/Api/Controller.php b/app/Http/Controllers/Api/Controller.php new file mode 100644 index 00000000..5029ee9f --- /dev/null +++ b/app/Http/Controllers/Api/Controller.php @@ -0,0 +1,15 @@ +get(); + $citiesWithProvidersCount = $citiesWithProviders->count(); + + $citiesWithProvidersIds = $citiesWithProviders->pluck("city_id"); + $countriesWithCitiesWithProvidersIds = City::whereIn("id", $citiesWithProvidersIds)->distinct()->pluck("country_id"); + $countriesWithCitiesWithProvidersCount = Country::whereIn("id", $countriesWithCitiesWithProvidersIds)->count(); + + $providersCount = Provider::count(); + + $providerCitiesCount = $citiesWithProviders + ->pluck("provider_name") + ->countBy() + ->map(function (int $count, string $name): array { + return [ + "name" => $name, + "count" => $count, + ]; + }) + ->sortByDesc("count") + ->values() + ->all(); + + $providers = ProviderResource::collection(Provider::all()); + + return response()->json([ + "usersCount" => $usersCount, + "citiesWithProvidersCount" => $citiesWithProvidersCount, + "countriesWithCitiesWithProvidersCount" => $countriesWithCitiesWithProvidersCount, + "providersCount" => $providersCount, + "providerCitiesCount" => $providerCitiesCount, + "providers" => $providers, + ]); + } +} diff --git a/app/Http/Controllers/Api/FavoritesController.php b/app/Http/Controllers/Api/FavoritesController.php new file mode 100644 index 00000000..38b49116 --- /dev/null +++ b/app/Http/Controllers/Api/FavoritesController.php @@ -0,0 +1,63 @@ +user(); + + $favoriteCities = $user->favorites()->with(["city.country", "city.cityProviders"])->get(); + + $cities = $favoriteCities->map(fn($favorite) => CityResource::make($favorite->city)); + + $providers = Provider::all(); + + return response()->json([ + "cities" => $cities, + "providers" => ProviderResource::collection($providers), + ]); + } + + public function store(Request $request): JsonResponse + { + $cityId = $request->input("city_id"); + $userId = $request->user()?->id; + + $favorite = Favorites::firstOrCreate([ + "user_id" => $userId, + "city_id" => $cityId, + ]); + + if ($favorite->wasRecentlyCreated) { + return response()->json([ + "message" => "City added to favorites.", + ]); + } + $favorite->delete(); + + return response()->json([ + "message" => "City removed from favorites.", + ]); + } + + public function check(Request $request, int $cityId): bool + { + $userId = $request->user()->id; + + return Favorites::where("user_id", $userId) + ->where("city_id", $cityId) + ->exists(); + } +} diff --git a/app/Http/Controllers/FavoritesController.php b/app/Http/Controllers/FavoritesController.php index e3aa021d..8314b4ad 100644 --- a/app/Http/Controllers/FavoritesController.php +++ b/app/Http/Controllers/FavoritesController.php @@ -20,7 +20,10 @@ public function index(): Response { $user = Auth::user(); - $favoriteCities = $user->favorites()->with(["city.country", "city.cityProviders"])->get(); + $favoriteCities = $user + ->favorites() + ->with(["city.country", "city.cityProviders"]) + ->get(); $cities = $favoriteCities->map(fn($favorite) => CityResource::make($favorite->city)); @@ -37,9 +40,10 @@ public function store(Request $request, Session $session): void $cityId = $request->input("city_id"); $userId = $request->user()?->id; - $favorite = Favorites::firstOrCreate( - ["user_id" => $userId, "city_id" => $cityId], - ); + $favorite = Favorites::firstOrCreate([ + "user_id" => $userId, + "city_id" => $cityId, + ]); if ($favorite->wasRecentlyCreated) { $session->flash("message", "City added to favorites."); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index f1e055a7..27b96873 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -29,6 +29,9 @@ use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Session\Middleware\StartSession; use Illuminate\View\Middleware\ShareErrorsFromSession; +use Laravel\Sanctum\Http\Middleware\CheckAbilities; +use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility; +use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful; use Spatie\Permission\Middleware\RoleMiddleware; class Kernel extends HttpKernel @@ -69,6 +72,7 @@ class Kernel extends HttpKernel "api" => [ ThrottleRequests::class . ":api", SubstituteBindings::class, + EnsureFrontendRequestsAreStateful::class, ], ]; @@ -91,5 +95,7 @@ class Kernel extends HttpKernel "throttle" => ThrottleRequests::class, "verified" => EnsureEmailIsVerified::class, "role" => RoleMiddleware::class, + "ability" => CheckAbilities::class, + "any-ability" => CheckForAnyAbility::class, ]; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index fffe88be..2d9544a4 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -6,6 +6,8 @@ use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\ServiceProvider; +use Laravel\Sanctum\PersonalAccessToken; +use Laravel\Sanctum\Sanctum; class AppServiceProvider extends ServiceProvider { @@ -26,5 +28,6 @@ public function register(): void public function boot(): void { JsonResource::withoutWrapping(); + Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); } } diff --git a/composer.json b/composer.json index d18e36a7..f0b43aae 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,9 @@ "license": "MIT", "require": { "php": "^8.2", + "ext-dom": "*", "ext-pdo": "*", + "dedoc/scramble": "^0.9.0", "guzzlehttp/guzzle": "^7.7", "inertiajs/inertia-laravel": "^0.6.9", "laravel/framework": "^10.13.0", @@ -16,8 +18,7 @@ "sentry/sentry-laravel": "^3.7", "spatie/laravel-permission": "^6.1", "stichoza/google-translate-php": "^5.1", - "symfony/dom-crawler": "^6.3", - "ext-dom": "*" + "symfony/dom-crawler": "^6.3" }, "require-dev": { "blumilksoftware/codestyle": "v2.5.0", diff --git a/composer.lock b/composer.lock index bbf4cc3d..c90d5a06 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f3bba854780a73912a8f7fad1136f625", + "content-hash": "785527ba25725291cee31cea1f845d2b", "packages": [ { "name": "brick/math", @@ -196,6 +196,80 @@ ], "time": "2023-12-20T15:40:13+00:00" }, + { + "name": "dedoc/scramble", + "version": "v0.9.0", + "source": { + "type": "git", + "url": "https://github.com/dedoc/scramble.git", + "reference": "6280da6809eecaa03243d726b957cc174b1ccb70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dedoc/scramble/zipball/6280da6809eecaa03243d726b957cc174b1ccb70", + "reference": "6280da6809eecaa03243d726b957cc174b1ccb70", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0", + "nikic/php-parser": "^5.0", + "php": "^8.1", + "phpstan/phpdoc-parser": "^1.0", + "spatie/laravel-package-tools": "^1.9.2" + }, + "require-dev": { + "laravel/pint": "^v1.1.0", + "nunomaduro/collision": "^7.0|^8.0", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.34", + "pestphp/pest-plugin-laravel": "^2.3", + "phpunit/phpunit": "^10.5", + "spatie/pest-plugin-snapshots": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Dedoc\\Scramble\\ScrambleServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Dedoc\\Scramble\\": "src", + "Dedoc\\Scramble\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Lytvynenko", + "email": "litvinenko95@gmail.com", + "role": "Developer" + } + ], + "description": "Automatic generation of API documentation for Laravel applications.", + "homepage": "https://github.com/dedoc/scramble", + "keywords": [ + "documentation", + "laravel", + "openapi" + ], + "support": { + "issues": "https://github.com/dedoc/scramble/issues", + "source": "https://github.com/dedoc/scramble/tree/v0.9.0" + }, + "funding": [ + { + "url": "https://github.com/romalytvynenko", + "type": "github" + } + ], + "time": "2024-03-11T19:27:28+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.2", @@ -3394,6 +3468,53 @@ ], "time": "2023-11-12T21:59:55+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.27.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/86e4d5a4b036f8f0be1464522f4c6b584c452757", + "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.27.0" + }, + "time": "2024-03-21T13:14:53+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -4359,6 +4480,66 @@ ], "time": "2023-10-12T14:38:46+00:00" }, + { + "name": "spatie/laravel-package-tools", + "version": "1.16.4", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53", + "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^9.28|^10.0|^11.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0", + "pestphp/pest": "^1.22", + "phpunit/phpunit": "^9.5.24", + "spatie/pest-plugin-test-time": "^1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.4" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-03-20T07:29:11+00:00" + }, { "name": "spatie/laravel-permission", "version": "6.3.0", @@ -10357,9 +10538,9 @@ "prefer-lowest": false, "platform": { "php": "^8.2", - "ext-pdo": "*", - "ext-dom": "*" + "ext-dom": "*", + "ext-pdo": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/config/auth.php b/config/auth.php index cf69f129..e0bdd12b 100644 --- a/config/auth.php +++ b/config/auth.php @@ -9,6 +9,10 @@ ], "guards" => [ + "api" => [ + "driver" => "sanctum", + "provider" => "users", + ], "web" => [ "driver" => "session", "provider" => "users", diff --git a/config/sanctum.php b/config/sanctum.php index 09cc519c..9c241d06 100644 --- a/config/sanctum.php +++ b/config/sanctum.php @@ -13,7 +13,7 @@ "guard" => ["web"], - "expiration" => null, + "expiration" => 720, "middleware" => [ "verify_csrf_token" => App\Http\Middleware\VerifyCsrfToken::class, diff --git a/config/scramble.php b/config/scramble.php new file mode 100644 index 00000000..9c9d14c0 --- /dev/null +++ b/config/scramble.php @@ -0,0 +1,27 @@ + "api", + "api_domain" => null, + "export_path" => "api.json", + "theme" => "light", + "info" => [ + "version" => env("API_VERSION", "0.0.1"), + "description" => "", + ], + "ui" => [ + "hide_try_it" => false, + "logo" => "", + "try_it_credentials_policy" => "include", + ], + "servers" => null, + "middleware" => [ + "web", + RestrictedDocsAccess::class, + ], + "extensions" => [], +]; diff --git a/lang/pl.json b/lang/pl.json index 0910749d..ad3e37d9 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -67,6 +67,8 @@ "of": "z", "to": "do", "results": "wyników", + "Language has been changed.": "Język został zmieniony.", + "Error changing language.": "Błąd podczas zmiany języka.", "Alternative name": "Alternatywna nazwa", "Filling map with providers...": "Wypełnianie mapy dostawcami...", "Didn't find any providers.": "Nie znaleziono dostawców.", @@ -141,6 +143,9 @@ "Cities with no country assigned": "Miasta bez przypisanego kraju", "Cities with no coordinates assigned": "Miasta bez przypisanych współrzędnych", "Delete all cities with no country assigned": "Usuń wszystkie miasta bez przypisanego kraju", + "All cities removed successfully!" : "Wszystkie miasta usunięte pomyślnie!", + "City alternative name created successfully.": "Alternatywna nazwa miasta utworzona pomyślnie", + "City alternative name deleted successfully.": "Alternatywna nazwa miasta usunięta pomyślnie", "Opinion added successfully.": "Opinia dodana pomyślnie.", "There was an error adding your opinion.": "Wystąpił błąd podczas dodawania opinii.", "Opinion edited successfully.": "Opinia zaktualizowana pomyślnie.", @@ -162,6 +167,7 @@ "You have logged in successfully.": "Zalogowano pomyślnie.", "You have logged out successfully.": "Wylogowano pomyślnie.", "You have registered successfully.": "Zarejestrowano pomyślnie", + "Login failed.": "Logowanie nie powiodło się.", "Please, rate that city": "Oceń miasto", "You can also login by:": "Możesz również zalogować się przez:", diff --git a/routes/api.php b/routes/api.php index 87836c77..9711e31c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,11 +2,58 @@ declare(strict_types=1); -use App\Http\Controllers\CityProviderController; +use App\Http\Controllers\Api\Admin\CityAlternativeNameController; +use App\Http\Controllers\Api\Admin\CityController; +use App\Http\Controllers\Api\Admin\CountryController; +use App\Http\Controllers\Api\Admin\ImportInfoController; +use App\Http\Controllers\Api\Admin\ProviderController; +use App\Http\Controllers\Api\AuthController; +use App\Http\Controllers\Api\ChangeLocaleController; +use App\Http\Controllers\Api\CityOpinionController; +use App\Http\Controllers\Api\CityPageController; +use App\Http\Controllers\Api\CityProviderController; +use App\Http\Controllers\Api\CityWithoutAssignedCountryController; +use App\Http\Controllers\Api\DashboardController; +use App\Http\Controllers\Api\FavoritesController; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; -Route::middleware("auth:sanctum")->get("/user", fn(Request $request): JsonResponse => new JsonResponse($request->user())); - +Route::middleware("auth:api")->get("/user", fn(Request $request): JsonResponse => new JsonResponse($request->user())); Route::get("/providers", [CityProviderController::class, "index"]); + +Route::middleware("guest")->group(function (): void { + Route::post("/register", [AuthController::class, "store"])->name("register"); + Route::post("/login", [AuthController::class, "login"])->name("login"); +}); + +Route::middleware("auth:sanctum")->group(function (): void { + Route::post("/logout", [AuthController::class, "logout"])->name("logout"); + + Route::post("/favorites", [FavoritesController::class, "store"]); + Route::get("/favorites/{city_id}", [FavoritesController::class, "check"]); + Route::get("/favorite-cities", [FavoritesController::class, "index"]); + + Route::post("/opinions", [CityOpinionController::class, "store"]); + Route::patch("/opinions/{cityOpinion}", [CityOpinionController::class, "update"]); + Route::delete("/opinions/{cityOpinion}", [CityOpinionController::class, "destroy"]); + + Route::middleware("ability:HasAdminRole")->group(function (): void { + Route::get("/admin/importers", [ImportInfoController::class, "index"]); + Route::resource("/admin/providers", ProviderController::class); + Route::resource("/admin/countries", CountryController::class); + Route::resource("/admin/cities", CityController::class); + Route::resource("/admin/dashboard", DashboardController::class); + Route::resource("/city-alternative-name", CityAlternativeNameController::class); + Route::patch("/update-city-providers/{city}", [CityProviderController::class, "update"]); + + Route::post("/run-importers", [CityProviderController::class, "runImporters"]); + Route::delete("/delete-city-without-assigned-country/{city}", [CityWithoutAssignedCountryController::class, "destroy"]); + Route::post("/delete-all-cities-without-assigned-country", [CityWithoutAssignedCountryController::class, "destroyAll"]); + }); +}); +Route::post("/language/{locale}", ChangeLocaleController::class); + +Route::get("/{country:slug}/{city:slug}", [CityPageController::class, "index"]); + +Route::get("/images/providers/{filename}", [ProviderController::class, "showLogo"]); From 911b999fcafd5ee3b348efc944867eb251c18950 Mon Sep 17 00:00:00 2001 From: Aleksandra Kozubal <104600942+AleksandraKozubal@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:30:53 +0200 Subject: [PATCH 2/3] - fix api routes (#221) * fix api routes * lint --- routes/api.php | 68 ++++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/routes/api.php b/routes/api.php index 9711e31c..9db2f5a1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -19,41 +19,43 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; -Route::middleware("auth:api")->get("/user", fn(Request $request): JsonResponse => new JsonResponse($request->user())); -Route::get("/providers", [CityProviderController::class, "index"]); +Route::name("api.")->group(function (): void { + Route::middleware("auth:api")->get("/user", fn(Request $request): JsonResponse => new JsonResponse($request->user())); + Route::get("/providers", [CityProviderController::class, "index"]); -Route::middleware("guest")->group(function (): void { - Route::post("/register", [AuthController::class, "store"])->name("register"); - Route::post("/login", [AuthController::class, "login"])->name("login"); -}); + Route::middleware("guest")->group(function (): void { + Route::post("/register", [AuthController::class, "store"])->name("register"); + Route::post("/login", [AuthController::class, "login"])->name("login"); + }); -Route::middleware("auth:sanctum")->group(function (): void { - Route::post("/logout", [AuthController::class, "logout"])->name("logout"); - - Route::post("/favorites", [FavoritesController::class, "store"]); - Route::get("/favorites/{city_id}", [FavoritesController::class, "check"]); - Route::get("/favorite-cities", [FavoritesController::class, "index"]); - - Route::post("/opinions", [CityOpinionController::class, "store"]); - Route::patch("/opinions/{cityOpinion}", [CityOpinionController::class, "update"]); - Route::delete("/opinions/{cityOpinion}", [CityOpinionController::class, "destroy"]); - - Route::middleware("ability:HasAdminRole")->group(function (): void { - Route::get("/admin/importers", [ImportInfoController::class, "index"]); - Route::resource("/admin/providers", ProviderController::class); - Route::resource("/admin/countries", CountryController::class); - Route::resource("/admin/cities", CityController::class); - Route::resource("/admin/dashboard", DashboardController::class); - Route::resource("/city-alternative-name", CityAlternativeNameController::class); - Route::patch("/update-city-providers/{city}", [CityProviderController::class, "update"]); - - Route::post("/run-importers", [CityProviderController::class, "runImporters"]); - Route::delete("/delete-city-without-assigned-country/{city}", [CityWithoutAssignedCountryController::class, "destroy"]); - Route::post("/delete-all-cities-without-assigned-country", [CityWithoutAssignedCountryController::class, "destroyAll"]); + Route::middleware("auth:sanctum")->group(function (): void { + Route::post("/logout", [AuthController::class, "logout"])->name("logout"); + + Route::post("/favorites", [FavoritesController::class, "store"]); + Route::get("/favorites/{city_id}", [FavoritesController::class, "check"]); + Route::get("/favorite-cities", [FavoritesController::class, "index"]); + + Route::post("/opinions", [CityOpinionController::class, "store"]); + Route::patch("/opinions/{cityOpinion}", [CityOpinionController::class, "update"]); + Route::delete("/opinions/{cityOpinion}", [CityOpinionController::class, "destroy"]); + + Route::middleware("ability:HasAdminRole")->group(function (): void { + Route::get("/admin/importers", [ImportInfoController::class, "index"]); + Route::resource("/admin/providers", ProviderController::class); + Route::resource("/admin/countries", CountryController::class); + Route::resource("/admin/cities", CityController::class); + Route::resource("/admin/dashboard", DashboardController::class); + Route::resource("/city-alternative-name", CityAlternativeNameController::class); + Route::patch("/update-city-providers/{city}", [CityProviderController::class, "update"]); + + Route::post("/run-importers", [CityProviderController::class, "runImporters"]); + Route::delete("/delete-city-without-assigned-country/{city}", [CityWithoutAssignedCountryController::class, "destroy"]); + Route::post("/delete-all-cities-without-assigned-country", [CityWithoutAssignedCountryController::class, "destroyAll"]); + }); }); -}); -Route::post("/language/{locale}", ChangeLocaleController::class); + Route::post("/language/{locale}", ChangeLocaleController::class); -Route::get("/{country:slug}/{city:slug}", [CityPageController::class, "index"]); + Route::get("/{country:slug}/{city:slug}", [CityPageController::class, "index"]); -Route::get("/images/providers/{filename}", [ProviderController::class, "showLogo"]); + Route::get("/images/providers/{filename}", [ProviderController::class, "showLogo"]); +}); From 39e2e609d569f979cffc7accd17c637c3148b9dc Mon Sep 17 00:00:00 2001 From: Artur Flis <102246039+Lee0z@users.noreply.github.com> Date: Mon, 8 Apr 2024 18:40:18 +0200 Subject: [PATCH 3/3] #169 - ability to edit and delete opinions for administrator (#201) * Create event * Add button url * Fix url * Secure backend from unauthorised changes * logic for admin to delete comments * lint * pencil is not displayed for admin * cleaned update opinion method on backend * Fix unnecessary commits and cs * Fix DataImporter.php * Added policies * Added validation and fixed policy * change policy to bool * Change policy logic * fix updating opinion * Fix policy --------- Co-authored-by: JakubKermes Co-authored-by: zmigroo --- .../Controllers/CityOpinionController.php | 13 ++++-------- app/Importers/DataImporter.php | 1 + app/Policies/CityOpinionPolicy.php | 21 +++++++++++++++++++ app/Providers/AppServiceProvider.php | 4 ++++ resources/js/Pages/City/Index.vue | 1 - resources/js/Shared/Components/Opinion.vue | 5 +++-- routes/web.php | 6 ++++-- 7 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 app/Policies/CityOpinionPolicy.php diff --git a/app/Http/Controllers/CityOpinionController.php b/app/Http/Controllers/CityOpinionController.php index deffeed6..82673821 100644 --- a/app/Http/Controllers/CityOpinionController.php +++ b/app/Http/Controllers/CityOpinionController.php @@ -6,24 +6,19 @@ use App\Http\Requests\CityOpinionRequest; use App\Models\CityOpinion; -use Illuminate\Support\Facades\Auth; class CityOpinionController extends Controller { public function store(CityOpinionRequest $request): void { - $opinion = $request->only(["rating", "content", "city_id"]); - $opinion["user_id"] = Auth::id(); - - CityOpinion::query()->create($opinion); + $request->user() + ->cityOpinions() + ->create($request->validated()); } public function update(CityOpinionRequest $request, CityOpinion $cityOpinion): void { - $opinion = $request->only(["rating", "content", "city_id"]); - $opinion["user_id"] = Auth::id(); - - $cityOpinion->update($opinion); + $cityOpinion->update($request->validated()); } public function destroy(CityOpinion $cityOpinion): void diff --git a/app/Importers/DataImporter.php b/app/Importers/DataImporter.php index 0d33a03a..226d4e05 100644 --- a/app/Importers/DataImporter.php +++ b/app/Importers/DataImporter.php @@ -90,6 +90,7 @@ protected function deleteMissingProviders(string $providerName, array $existingC ->whereNotIn("city_id", $existingCityProviders) ->whereNot("created_by", "admin") ->get(); + $cityProvidersToDelete->each(fn($cityProvider) => $cityProvider->delete()); } diff --git a/app/Policies/CityOpinionPolicy.php b/app/Policies/CityOpinionPolicy.php new file mode 100644 index 00000000..40f3a61a --- /dev/null +++ b/app/Policies/CityOpinionPolicy.php @@ -0,0 +1,21 @@ +user_id === $user->id; + } + + public function delete(User $user, CityOpinion $cityOpinion): bool + { + return $cityOpinion->user_id === $user->id || $user->hasRole("admin"); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 2d9544a4..0854d681 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,7 +4,10 @@ namespace App\Providers; +use App\Models\CityOpinion; +use App\Policies\CityOpinionPolicy; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; use Laravel\Sanctum\PersonalAccessToken; use Laravel\Sanctum\Sanctum; @@ -28,6 +31,7 @@ public function register(): void public function boot(): void { JsonResource::withoutWrapping(); + Gate::policy(CityOpinion::class, CityOpinionPolicy::class); Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); } } diff --git a/resources/js/Pages/City/Index.vue b/resources/js/Pages/City/Index.vue index 023550da..21ebec50 100644 --- a/resources/js/Pages/City/Index.vue +++ b/resources/js/Pages/City/Index.vue @@ -16,7 +16,6 @@ import InfoPopup from '@/Shared/Components/InfoPopup.vue' import Opinion from '@/Shared/Components/Opinion.vue' const toast = useToast() - const page = usePage() const isAuth = computed(() => page.props.auth.isAuth) diff --git a/resources/js/Shared/Components/Opinion.vue b/resources/js/Shared/Components/Opinion.vue index f65ba8d0..30236a99 100644 --- a/resources/js/Shared/Components/Opinion.vue +++ b/resources/js/Shared/Components/Opinion.vue @@ -8,6 +8,7 @@ import DeleteModal from './DeleteModal.vue' import { useToast } from 'vue-toastification' import ErrorMessage from './ErrorMessage.vue' +const isAdmin = computed(() => page.props.auth.isAdmin) const toast = useToast() const page = usePage() const user = computed(() => page.props.auth.user) @@ -103,8 +104,8 @@ const emptyRatingError = ref('') {{ opinion.content }} -
-