diff --git a/.env.dist b/.env.dist index d258603a..c1852bb1 100644 --- a/.env.dist +++ b/.env.dist @@ -7,10 +7,10 @@ APP_URL= DB_CONNECTION=sqlite BROADCAST_DRIVER=log -CACHE_DRIVER=file +CACHE_DRIVER=redis FILESYSTEM_DISK=local QUEUE_CONNECTION=sync -SESSION_DRIVER=file +SESSION_DRIVER=redis SESSION_LIFETIME=120 REDIS_HOST=127.0.0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a94f1ee..10c87650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# v0.5.0 +- Use redis for session and cache +- Automatically select some ingredients when running the application for the first time +- Add OpenAPI specification and `/docs` route +- Fixed an error response when adding ingredient to the shelf from shopping list +- Updated some endpoints to be more consistent +- Include substitute ingredients when showing a list of shelf cocktails +- Added debugbar +- Remove the need to run `chown` in docker container +- Add demo environment support + # v0.4.1 - Enable opcache in docker image - Cache route and config in docker image diff --git a/Dockerfile b/Dockerfile index 1415123d..010fd2de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,9 @@ RUN apt update \ && apt-get autoremove -y \ && apt-get clean -RUN docker-php-ext-install opcache +RUN docker-php-ext-install opcache \ + && pecl install redis \ + && docker-php-ext-enable redis # Setup default apache stuff RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf @@ -27,6 +29,8 @@ RUN chmod +x /usr/local/bin/entrypoint # Add composer COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer +USER www-data:www-data + WORKDIR /var/www/cocktails RUN git clone https://github.com/karlomikus/bar-assistant.git . diff --git a/README.md b/README.md index be76c859..db99724f 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,11 @@ ## šŸø Bar assistant -Bar assistant is a self hosted application for managing your home bar. It allows you to add ingredients and create custom cocktail recipes. +Bar assistant is a self hosted application for managing your home bar. It allows you to add your ingredients, search for cocktails and create custom cocktail recipes. This repository only contains the API server, if you are looking for easy to use web client, take a look at [Salt Rim](https://github.com/karlomikus/vue-salt-rim). -Note: This application is still in development and there will be breaking changes and loss of data. I do not recommend using this in a "production" environment until a stable version is released. - ## Features - - Includes all current IBA cocktails - Over 100 ingredients - Endpoints for managing of ingredients and cocktails @@ -26,32 +23,55 @@ Note: This application is still in development and there will be breaking change - Ability to upload and assign images - Shopping list for missing ingredients - Automatic indexing of data in Meilisearch +- Cocktail ingredient substitutes +- Assign glass types to cocktails ## Planned features - - Cocktail recipe sharing +- Cocktail and shopping list printing - User defined cocktail collections - Cocktail ratings - Add user notes to cocktail - Add cocktail flavor profiles -- Ingredient and cocktail aliasing -- Ingredient substitutes +- Cocktail recipe scraping +- Importing and exporting cocktails ## Installation -This application is made with Laravel, so you should [follow installation instructions](https://laravel.com/docs/9.x/deployment) for a standard Laravel project. +This application is made with Laravel, so you should check out [deployment requirements](https://laravel.com/docs/9.x/deployment) for a standard Laravel project. -### Requirements: +The basic requirements are: -- PHP >=8.1 +- PHP >= 8.1 - Sqlite 3 -- Working [Meilisearch server](https://github.com/meilisearch) +- Working [Meilisearch server](https://github.com/meilisearch) instance +- (Optional) Redis server instance + +## Docker setup + +Docker setup is the easiest way to get started. This will run only the server but you can [checkout how to setup the whole Bar Assistant stack here.](https://github.com/bar-assistant/docker) + +``` bash +$ docker volume create bass-volume + +$ docker run -d \ + --name bar-assistant \ + -e APP_URL=http://localhost:8000 \ + -e MEILISEARCH_HOST=http://localhost:7700 \ + -e MEILISEARCH_KEY=masterKey \ + -e REDIS_HOST=redis \ + -v bass-volume:/var/www/cocktails/storage \ + -p 8000:80 \ + kmikus12/bar-assistant-server +``` + +Docker image exposes the `/var/www/cocktails/storage` volume, and there is currently and issue with host permissions if you are using a local folder mapping. ### Meilisearch -Bar Assistant is using Meilisearch as a primary Scout driver. It's used to index cocktails and ingredients used for filtering and full text search. +Bar Assistant is using Meilisearch as a primary [Scout driver](https://laravel.com/docs/9.x/scout). It's main purpose is to index cocktails and ingredients and power filtering and searching on the frontend. Checkout [this guide here](https://docs.meilisearch.com/learn/cookbooks/docker.html) on how to setup Meilisearch docker instance. -### Setup +## Manual setup After cloning the repository, you should do the following: @@ -68,6 +88,12 @@ APP_URL= MEILISEARCH_HOST= # Meilisearch search key MEILISEARCH_KEY= +# If using redis, the following +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 +CACHE_DRIVER=redis +SESSION_DRIVER=redis ``` 2. Run the commands @@ -79,32 +105,23 @@ $ php artisan key:generate $ php artisan storage:link # To setup the database: -$ php artisan migrate +$ php artisan migrate --force # To fill the database with data $ php artisan bar:open ``` -Default login information is: +## Usage + +Checkout `/docs` route to see endpoints documentation. +Default login information is: - email: `admin@example.com` - password: `password` -## Docker - -[Also checkout how to setup the whole Bar Assistant stack here.](https://github.com/karlomikus/vue-salt-rim#docker-compose) - -``` bash -docker run -d \ - -e APP_URL=http://localhost:8080 \ - -e MEILISEARCH_HOST=http://localhost:7700 \ - -e MEILISEARCH_KEY=maskerKey \ - kmikus12/bar-assistant-server -``` - ## Contributing -TODO +TODO, Fork and create a pull request... ## License diff --git a/app/Console/Commands/BarSearchRefresh.php b/app/Console/Commands/BarSearchRefresh.php new file mode 100644 index 00000000..382220d5 --- /dev/null +++ b/app/Console/Commands/BarSearchRefresh.php @@ -0,0 +1,48 @@ +info('Removing cocktails and ingredients index...'); + Artisan::call('scout:flush', ['model' => "Kami\Cocktail\Models\Cocktail"]); + Artisan::call('scout:flush', ['model' => "Kami\Cocktail\Models\Ingredient"]); + + // Update settings + $this->info('Updating index settings...'); + SearchActions::updateIndexSettings(); + + $this->info('Importing cocktails and ingredients...'); + Artisan::call('scout:import', ['model' => "Kami\Cocktail\Models\Cocktail"]); + Artisan::call('scout:import', ['model' => "Kami\Cocktail\Models\Ingredient"]); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/OpenBar.php b/app/Console/Commands/OpenBar.php index 01691bfd..c8c644fb 100644 --- a/app/Console/Commands/OpenBar.php +++ b/app/Console/Commands/OpenBar.php @@ -11,6 +11,7 @@ use Symfony\Component\Yaml\Yaml; use Illuminate\Support\Facades\DB; use Kami\Cocktail\Models\Cocktail; +use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Hash; use Kami\Cocktail\Models\Ingredient; use Illuminate\Database\Eloquent\Model; @@ -57,7 +58,7 @@ public function handle() DB::table('users')->insert([ [ 'name' => 'BAR ASSISTANT BOT', - 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + 'password' => '', // password 'email' => 'bot@my-bar.localhost', 'email_verified_at' => null, 'remember_token' => null, @@ -69,7 +70,7 @@ public function handle() 'email' => $this->argument('email'), 'email_verified_at' => now(), 'remember_token' => Str::random(10), - 'search_api_key' => SearchActions::getPublicApiKey() // TODO: Check if already exists in ENV + 'search_api_key' => App::environment('demo') ? SearchActions::getPublicDemoApiKey() : SearchActions::getPublicApiKey() // TODO: Check if already exists in ENV ] ]); @@ -115,8 +116,8 @@ public function handle() $this->info('Filling your bar with ingredients...'); // Fruits - Ingredient::create(['name' => 'Lime', 'ingredient_category_id' => $fruits->id, 'strength' => 0.0, 'description' => 'Lime fruit', 'user_id' => 1]); - Ingredient::create(['name' => 'Lemon', 'ingredient_category_id' => $fruits->id, 'strength' => 0.0, 'description' => 'Lemon fruit', 'user_id' => 1]); + $limeFruit = Ingredient::create(['name' => 'Lime', 'ingredient_category_id' => $fruits->id, 'strength' => 0.0, 'description' => 'Lime fruit', 'user_id' => 1]); + $lemonFruit = Ingredient::create(['name' => 'Lemon', 'ingredient_category_id' => $fruits->id, 'strength' => 0.0, 'description' => 'Lemon fruit', 'user_id' => 1]); Ingredient::create(['name' => 'Orange', 'ingredient_category_id' => $fruits->id, 'strength' => 0.0, 'description' => 'Orange fruit', 'user_id' => 1]); Ingredient::create(['name' => 'Pineapple', 'ingredient_category_id' => $fruits->id, 'strength' => 0.0, 'description' => 'Pineapple fruit', 'user_id' => 1]); Ingredient::create(['name' => 'Apple', 'ingredient_category_id' => $fruits->id, 'strength' => 0.0, 'description' => 'Apple fruit', 'user_id' => 1]); @@ -128,13 +129,13 @@ public function handle() // Misc Ingredient::create(['name' => 'White Peach Puree', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'A purĆ©e (or mash) is cooked food, usually vegetables, fruits or legumes, that has been ground, pressed, blended or sieved to the consistency of a creamy paste or liquid.', 'user_id' => 1]); Ingredient::create(['name' => 'Cream', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Cream is a dairy product composed of the higher-fat layer skimmed from the top of milk before homogenization.', 'user_id' => 1]); - Ingredient::create(['name' => 'Salt', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Salt', 'user_id' => 1]); - Ingredient::create(['name' => 'Pepper', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Black pepper', 'user_id' => 1]); + $salt = Ingredient::create(['name' => 'Salt', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Salt', 'user_id' => 1]); + $pepper = Ingredient::create(['name' => 'Pepper', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Black pepper', 'user_id' => 1]); Ingredient::create(['name' => 'Tabasco', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Hot sauce made from vinegar, tabasco peppers, and salt.', 'user_id' => 1]); Ingredient::create(['name' => 'Worcestershire Sauce', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Fermented liquid condiment created in the city of Worcester', 'user_id' => 1]); - Ingredient::create(['name' => 'Sugar', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'White sugar', 'user_id' => 1]); - Ingredient::create(['name' => 'Egg White', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Chicken egg without yolk.', 'user_id' => 1]); - Ingredient::create(['name' => 'Egg Yolk', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Yolk from chicken egg', 'user_id' => 1]); + $sugar = Ingredient::create(['name' => 'Sugar', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'White sugar', 'user_id' => 1]); + $eggWhite = Ingredient::create(['name' => 'Egg White', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Chicken egg without yolk.', 'user_id' => 1]); + $eggYolk = Ingredient::create(['name' => 'Egg Yolk', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Yolk from chicken egg', 'user_id' => 1]); Ingredient::create(['name' => 'Coconut Cream', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Opaque, milky-white liquid extracted from the grated pulp of mature coconuts.', 'user_id' => 1]); Ingredient::create(['name' => 'Vanilla Extract', 'ingredient_category_id' => $uncategorized->id, 'strength' => 0.0, 'description' => 'Solution made by macerating and percolating vanilla pods in a solution of ethanol and water.', 'user_id' => 1]); @@ -175,14 +176,15 @@ public function handle() Ingredient::create(['name' => 'PassoĆ£', 'ingredient_category_id' => $liqueurs->id, 'strength' => 17.0, 'description' => 'Liqueur with passion fruit being the main ingredient.', 'color' => '#ea5f4c', 'origin' => 'France', 'user_id' => 1]); Ingredient::create(['name' => 'Fernet Branca', 'ingredient_category_id' => $liqueurs->id, 'strength' => 39.0, 'description' => 'Fernet Branca is a bittersweet, herbal liqueur made with a number of different herbs and spices, including myrrh, rhubarb, chamomile, cardamom, aloe, and gentian root.', 'origin' => 'Italy', 'user_id' => 1]); Ingredient::create(['name' => 'Baileys Irish Cream', 'ingredient_category_id' => $liqueurs->id, 'strength' => 17.0, 'description' => 'Baileys Irish Cream is an Irish cream liqueur, an alcoholic drink flavoured with cream, cocoa and Irish whiskey. It is made by Diageo at Nangor Road, in Dublin, Ireland and in Mallusk, Northern Ireland. It is the original Irish cream, invented by a team headed by Tom Jago in 1971 for Gilbeys of Ireland.', 'origin' => 'Ireland', 'user_id' => 1]); + Ingredient::create(['name' => 'St-Germain', 'ingredient_category_id' => $liqueurs->id, 'strength' => 20.0, 'description' => 'St-Germain is an elderflower liqueur It is made using the petals of Sambucus nigra from the Savoie region in France, and each bottle is numbered with the year the petals were collected. Petals are collected annually in the spring over a period of three to four weeks, and are often transported by bicycle to collection points to avoid damaging the petals and impacting the flavour.', 'color' => '#f8e888', 'origin' => 'France', 'user_id' => 1]); $curacao = Ingredient::create(['name' => 'Orange CuraƧao', 'ingredient_category_id' => $liqueurs->id, 'strength' => 20.0, 'description' => 'Liqueur flavored with the dried peel of the bitter orange laraha, a citrus fruit grown on the Dutch island of CuraƧao. CuraƧao is used by liqueur makers the world over as a generic name for orange-flavoured liqueurs.', 'color' => '#edaa53', 'origin' => 'Netherlands', 'user_id' => 1]); Ingredient::create(['name' => 'Dry CuraƧao', 'parent_ingredient_id' => $curacao->id, 'ingredient_category_id' => $liqueurs->id, 'strength' => 40.0, 'description' => 'Cognac Ferrand\'s innovative owner Alexandre Gabriel, followed an old recipe and modified it to create this curaƧao. While CuraƧao and sweet oranges are the main ingredients, vanilla, prunes and lemon peel are amongst the other botanicals called for in the old recipe.', 'origin' => 'Italy', 'user_id' => 1]); Ingredient::create(['name' => 'Blue CuraƧao', 'parent_ingredient_id' => $curacao->id, 'ingredient_category_id' => $liqueurs->id, 'strength' => 20.0, 'description' => 'Liqueur flavored with the dried peel of the bitter orange laraha, a citrus fruit grown on the Dutch island of CuraƧao.', 'color' => '#0192fe', 'origin' => 'Netherlands', 'user_id' => 1]); // Juices - Ingredient::create(['name' => 'Lemon juice', 'ingredient_category_id' => $juices->id, 'strength' => 0.0, 'description' => 'Freshly squeezed lemon juice.', 'color' => '#f3efda', 'user_id' => 1]); - Ingredient::create(['name' => 'Lime juice', 'ingredient_category_id' => $juices->id, 'strength' => 0.0, 'description' => 'Freshly squeezed lime juice.', 'color' => '#e9f1d7', 'user_id' => 1]); + $lemonJuice = Ingredient::create(['name' => 'Lemon juice', 'ingredient_category_id' => $juices->id, 'strength' => 0.0, 'description' => 'Freshly squeezed lemon juice.', 'color' => '#f3efda', 'user_id' => 1]); + $limeJuice = Ingredient::create(['name' => 'Lime juice', 'ingredient_category_id' => $juices->id, 'strength' => 0.0, 'description' => 'Freshly squeezed lime juice.', 'color' => '#e9f1d7', 'user_id' => 1]); Ingredient::create(['name' => 'Orange juice', 'ingredient_category_id' => $juices->id, 'strength' => 0.0, 'description' => 'Freshly squeezed orange juice.', 'color' => '#ff9518', 'user_id' => 1]); Ingredient::create(['name' => 'Grapefruit juice', 'ingredient_category_id' => $juices->id, 'strength' => 0.0, 'description' => 'Freshly squeezed grapefruit juice.', 'color' => '#ed7500', 'user_id' => 1]); Ingredient::create(['name' => 'Cranberry juice', 'ingredient_category_id' => $juices->id, 'strength' => 0.0, 'description' => 'Juice made from cranberries.', 'color' => '#9c0024', 'user_id' => 1]); @@ -192,13 +194,13 @@ public function handle() Ingredient::create(['name' => 'Chamomile cordial', 'ingredient_category_id' => $juices->id, 'strength' => 0.0, 'description' => 'Herbal juice made from chamomile.', 'color' => '#e2dccc', 'user_id' => 1]); // Beverages - Ingredient::create(['name' => 'Water', 'ingredient_category_id' => $beverages->id, 'strength' => 0.0, 'color' => '#fff', 'description' => 'It\'s water.', 'origin' => 'Worldwide', 'user_id' => 1]); - Ingredient::create(['name' => 'Club soda', 'ingredient_category_id' => $beverages->id, 'strength' => 0.0, 'color' => '#fff', 'description' => 'Club soda is a manufactured form of carbonated water, commonly used as a drink mixer.', 'origin' => 'Worldwide', 'user_id' => 1]); + $water = Ingredient::create(['name' => 'Water', 'ingredient_category_id' => $beverages->id, 'strength' => 0.0, 'color' => '#fff', 'description' => 'It\'s water.', 'origin' => 'Worldwide', 'user_id' => 1]); + $clubSoda = Ingredient::create(['name' => 'Club soda', 'ingredient_category_id' => $beverages->id, 'strength' => 0.0, 'color' => '#fff', 'description' => 'Club soda is a manufactured form of carbonated water, commonly used as a drink mixer.', 'origin' => 'Worldwide', 'user_id' => 1]); Ingredient::create(['name' => 'Tonic', 'ingredient_category_id' => $beverages->id, 'strength' => 0.0, 'color' => '#fff', 'description' => 'Tonic water (or Indian tonic water) is a carbonated soft drink in which quinine is dissolved.', 'origin' => 'Worldwide', 'user_id' => 1]); - Ingredient::create(['name' => 'Cola', 'ingredient_category_id' => $beverages->id, 'strength' => 0.0, 'color' => '#411919', 'description' => 'Cola is a carbonated soft drink flavored with vanilla, cinnamon, citrus oils and other flavorings.', 'origin' => 'Worldwide', 'user_id' => 1]); + $cola = Ingredient::create(['name' => 'Cola', 'ingredient_category_id' => $beverages->id, 'strength' => 0.0, 'color' => '#411919', 'description' => 'Cola is a carbonated soft drink flavored with vanilla, cinnamon, citrus oils and other flavorings.', 'origin' => 'Worldwide', 'user_id' => 1]); Ingredient::create(['name' => 'Ginger beer', 'ingredient_category_id' => $beverages->id, 'strength' => 0.0, 'color' => '#fff', 'description' => 'Ginger beer is a sweetened and carbonated, usually non-alcoholic beverage.', 'origin' => 'Worldwide', 'user_id' => 1]); Ingredient::create(['name' => 'Espresso', 'ingredient_category_id' => $beverages->id, 'strength' => 0.0, 'color' => '#fff', 'description' => 'Espresso is generally thicker than coffee brewed by other methods, with a viscosity similar to that of warm honey.', 'origin' => 'Italy', 'user_id' => 1]); - Ingredient::create(['name' => 'Coffee', 'ingredient_category_id' => $beverages->id, 'strength' => 0.0, 'color' => '#fff', 'description' => 'Coffee is a drink prepared from roasted coffee beans.', 'origin' => 'Africa', 'user_id' => 1]); + $coffee = Ingredient::create(['name' => 'Coffee', 'ingredient_category_id' => $beverages->id, 'strength' => 0.0, 'color' => '#fff', 'description' => 'Coffee is a drink prepared from roasted coffee beans.', 'origin' => 'Africa', 'user_id' => 1]); Ingredient::create(['name' => 'Orange Flower Water', 'ingredient_category_id' => $beverages->id, 'strength' => 0.0, 'color' => '#fff', 'description' => 'Clear aromatic by-product of the distillation of fresh bitter-orange blossoms for their essential oil.', 'origin' => 'Mediterranean', 'user_id' => 1]); // Spirits @@ -208,6 +210,7 @@ public function handle() $vodka = Ingredient::create(['name' => 'Vodka', 'ingredient_category_id' => $spirits->id, 'strength' => 40.0, 'color' => '#ffffff', 'description' => 'Clear alcoholic beverage distilled from cereal grains and potatos.', 'origin' => 'Russia', 'user_id' => 1]); Ingredient::create(['name' => 'Vanilla Vodka', 'parent_ingredient_id' => $vodka->id, 'ingredient_category_id' => $spirits->id, 'strength' => 40.0, 'color' => '#ffffff', 'description' => 'Vodka with added vanilla essence.', 'origin' => 'Russia', 'user_id' => 1]); + Ingredient::create(['name' => 'Vodka Citron', 'parent_ingredient_id' => $vodka->id, 'ingredient_category_id' => $spirits->id, 'strength' => 40.0, 'color' => '#ffffff', 'description' => 'Vodka with added lemon essence.', 'origin' => 'Sweden', 'user_id' => 1]); $whiskey = Ingredient::create(['name' => 'Whiskey', 'ingredient_category_id' => $spirits->id, 'strength' => 40.0, 'color' => '#d54a06', 'description' => 'Distilled alcoholic beverage made from fermented grain mash.', 'origin' => 'Worldwide', 'user_id' => 1]); Ingredient::create(['name' => 'Bourbon Whiskey', 'parent_ingredient_id' => $whiskey->id, 'ingredient_category_id' => $spirits->id, 'strength' => 40.0, 'color' => '#d54a06', 'description' => 'Barrel-aged distilled liquor made primarily from corn.', 'origin' => 'North America', 'user_id' => 1]); @@ -242,7 +245,7 @@ public function handle() Ingredient::create(['name' => 'Absinthe', 'ingredient_category_id' => $spirits->id, 'strength' => 40.0, 'color' => '#b7ca8e', 'description' => 'Anise-flavoured spirit derived from several plants, including wormwood.', 'origin' => 'France', 'user_id' => 1]); // Syrups - Ingredient::create(['name' => 'Simple Syrup', 'ingredient_category_id' => $syrups->id, 'description' => 'Syrup made with sugar and water. Usually in 1:1 or 2:1 ratio.', 'color' => '#e6dfcc', 'user_id' => 1]); + $simpleSyrup = Ingredient::create(['name' => 'Simple Syrup', 'ingredient_category_id' => $syrups->id, 'description' => 'Syrup made with sugar and water. Usually in 1:1 or 2:1 ratio.', 'color' => '#e6dfcc', 'user_id' => 1]); Ingredient::create(['name' => 'Gomme Syrup', 'ingredient_category_id' => $syrups->id, 'description' => 'A thicker simple syrup mixed with arabica gum powder.', 'color' => '#e6dfcc', 'user_id' => 1]); Ingredient::create(['name' => 'Orgeat Syrup', 'ingredient_category_id' => $syrups->id, 'description' => 'Sweet syrup made from almonds, sugar, and rose water or orange flower water.', 'color' => '#d9ca9f', 'user_id' => 1]); Ingredient::create(['name' => 'Honey Syrup', 'ingredient_category_id' => $syrups->id, 'description' => 'Syrup made from dissolving honey in water.', 'color' => '#f2a900', 'user_id' => 1]); @@ -282,7 +285,7 @@ public function handle() $ing->save(); } - $this->info('Finding some cocktail recipes...'); + $this->info('Importing cocktail recipes...'); $this->importCocktailsFromJson(resource_path('/data/iba_cocktails_v0.1.0.yml')); $this->importCocktailsFromJson(resource_path('/data/popular_cocktails.yml')); @@ -290,6 +293,33 @@ public function handle() Artisan::call('scout:import', ['model' => "Kami\Cocktail\Models\Cocktail"]); Artisan::call('scout:import', ['model' => "Kami\Cocktail\Models\Ingredient"]); + $this->info('Selecting standard ingredients...'); + $defaultUser = \Kami\Cocktail\Models\User::find(2); + $defaultUserIngredients = [ + $limeFruit, + $lemonFruit, + $salt, + $pepper, + $sugar, + $eggWhite, + $eggYolk, + $lemonJuice, + $limeJuice, + $water, + $clubSoda, + $cola, + $coffee, + $simpleSyrup, + ]; + + foreach ($defaultUserIngredients as $dui) { + $defaultUser->shelfIngredients()->save( + new \Kami\Cocktail\Models\UserIngredient([ + 'ingredient_id' => $dui->id + ]) + ); + } + Model::reguard(); $this->info('You are ready to serve!'); diff --git a/app/Http/Controllers/HealthController.php b/app/Http/Controllers/ServerController.php similarity index 61% rename from app/Http/Controllers/HealthController.php rename to app/Http/Controllers/ServerController.php index 1213c980..7df371e3 100644 --- a/app/Http/Controllers/HealthController.php +++ b/app/Http/Controllers/ServerController.php @@ -5,8 +5,15 @@ use Laravel\Scout\EngineManager; -class HealthController extends Controller +class ServerController extends Controller { + public function index() + { + return response()->json([ + 'status' => 'available' + ]); + } + public function version(EngineManager $engine) { /** @var \MeiliSearch\Client */ @@ -21,4 +28,13 @@ public function version(EngineManager $engine) ] ]); } + + public function openApi() + { + return response( + file_get_contents(base_path('docs/open-api-spec.yml')), + 200, + ['Content-Type' => 'application/x-yaml'] + ); + } } diff --git a/app/Http/Controllers/ShelfController.php b/app/Http/Controllers/ShelfController.php index 0458a021..3b386631 100644 --- a/app/Http/Controllers/ShelfController.php +++ b/app/Http/Controllers/ShelfController.php @@ -23,14 +23,19 @@ public function index(Request $request) public function save(Request $request, int $ingredientId) { - $userIngredient = new UserIngredient(); - $userIngredient->ingredient_id = $ingredientId; - + // Remove ingredient from the shopping list if it exists UserShoppingList::where('ingredient_id', $ingredientId)->delete(); - $si = $request->user()->shelfIngredients()->save($userIngredient); + // Check if ingredient is already in the shelf, and add it if it's not + if (!$request->user()->shelfIngredients->contains('ingredient_id', $ingredientId)) { + $userIngredient = new UserIngredient(); + $userIngredient->ingredient_id = $ingredientId; + $shelfIngredient = $request->user()->shelfIngredients()->save($userIngredient); + } else { + $shelfIngredient = $request->user()->shelfIngredients->where('ingredient_id', $ingredientId)->first(); + } - return new UserIngredientResource($si); + return new UserIngredientResource($shelfIngredient); } public function batch(UserIngredientBatchRequest $request) diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index e3ec3dbc..34cd9dbb 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -39,6 +39,7 @@ class Kernel extends HttpKernel ], 'api' => [ + \Kami\Cocktail\Http\Middleware\DisablePostOnDemoEnv::class, \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 'throttle:api', \Illuminate\Routing\Middleware\SubstituteBindings::class, diff --git a/app/Http/Middleware/DisablePostOnDemoEnv.php b/app/Http/Middleware/DisablePostOnDemoEnv.php new file mode 100644 index 00000000..131f28cc --- /dev/null +++ b/app/Http/Middleware/DisablePostOnDemoEnv.php @@ -0,0 +1,34 @@ +isMethodSafe() && !$request->routeIs($allowedDemoPostRoutes)) { + return response()->json([ + 'error' => 'api_error', + 'message' => 'Disabled on current environment.' + ], 405); + } + + return $next($request); + } +} diff --git a/app/Models/Cocktail.php b/app/Models/Cocktail.php index 8f7e124d..86432275 100644 --- a/app/Models/Cocktail.php +++ b/app/Models/Cocktail.php @@ -13,7 +13,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; -class Cocktail extends Model +class Cocktail extends Model implements SiteSearchable { use HasFactory, Searchable, HasImages, HasSlug; @@ -22,11 +22,11 @@ class Cocktail extends Model protected static function booted() { static::saved(function($cocktail) { - SearchActions::update($cocktail); + SearchActions::updateSearchIndex($cocktail); }); static::deleted(function($cocktail) { - SearchActions::delete($cocktail); + SearchActions::deleteSearchIndex($cocktail); }); } @@ -64,7 +64,7 @@ public function delete() return parent::delete(); } - public function toSiteSearchArray() + public function toSiteSearchArray(): array { return [ 'key' => 'co_' . (string) $this->id, diff --git a/app/Models/Ingredient.php b/app/Models/Ingredient.php index ffec177a..51f8283a 100644 --- a/app/Models/Ingredient.php +++ b/app/Models/Ingredient.php @@ -13,7 +13,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; -class Ingredient extends Model +class Ingredient extends Model implements SiteSearchable { use HasFactory, Searchable, HasImages, HasSlug; @@ -33,11 +33,11 @@ class Ingredient extends Model protected static function booted() { static::saved(function($ing) { - SearchActions::update($ing); + SearchActions::updateSearchIndex($ing); }); static::deleted(function($ing) { - SearchActions::delete($ing); + SearchActions::deleteSearchIndex($ing); }); } @@ -104,7 +104,7 @@ public function delete() return parent::delete(); } - public function toSiteSearchArray() + public function toSiteSearchArray(): array { return [ 'key' => 'in_' . (string) $this->id, diff --git a/app/Models/SiteSearchable.php b/app/Models/SiteSearchable.php new file mode 100644 index 00000000..507a5ef0 --- /dev/null +++ b/app/Models/SiteSearchable.php @@ -0,0 +1,9 @@ +engine(); @@ -21,7 +23,25 @@ public static function getPublicApiKey() return $key->getKey(); } - public static function checkHealth() + public static function getPublicDemoApiKey(): ?string + { + /** @var \Laravel\Scout\Engines\MeiliSearchEngine|\MeiliSearch\Client */ + $engine = app(\Laravel\Scout\EngineManager::class)->engine(); + + $key = $engine->createKey([ + 'actions' => [ + 'search' + ], + 'indexes' => ['cocktails', 'ingredients', 'site_search_index'], + 'expiresAt' => null, + 'name' => 'Bar Assistant DEMO', + 'description' => 'Client key generated by Bar Assistant Server' + ]); + + return $key->getKey(); + } + + public static function checkHealth(): ?array { /** @var \Laravel\Scout\Engines\MeiliSearchEngine|\MeiliSearch\Client */ $engine = app(\Laravel\Scout\EngineManager::class)->engine(); @@ -29,7 +49,7 @@ public static function checkHealth() return $engine->health(); } - public static function updateIndexSettings() + public static function updateIndexSettings(): void { /** @var \Laravel\Scout\Engines\MeiliSearchEngine|\MeiliSearch\Client */ $engine = app(\Laravel\Scout\EngineManager::class)->engine(); @@ -52,7 +72,7 @@ public static function updateIndexSettings() ]); } - public static function update($model) + public static function updateSearchIndex(SiteSearchable $model): void { /** @var \Laravel\Scout\Engines\MeiliSearchEngine|\MeiliSearch\Client */ $engine = app(\Laravel\Scout\EngineManager::class)->engine(); @@ -62,7 +82,7 @@ public static function update($model) ], 'key'); } - public static function delete($model) + public static function deleteSearchIndex(SiteSearchable $model): void { /** @var \Laravel\Scout\Engines\MeiliSearchEngine|\MeiliSearch\Client */ $engine = app(\Laravel\Scout\EngineManager::class)->engine(); @@ -70,7 +90,7 @@ public static function delete($model) $engine->index('site_search_index')->deleteDocument($model->toSiteSearchArray()['key']); } - public static function flushSearchIndex() + public static function flushSearchIndex(): void { /** @var \Laravel\Scout\Engines\MeiliSearchEngine|\MeiliSearch\Client */ $engine = app(\Laravel\Scout\EngineManager::class)->engine(); diff --git a/app/Services/CocktailService.php b/app/Services/CocktailService.php index b8708c04..71d5e4cd 100644 --- a/app/Services/CocktailService.php +++ b/app/Services/CocktailService.php @@ -249,8 +249,33 @@ public function getCocktailsByUserIngredients(int $userId) ->groupBy('c.id') ->havingRaw('SUM(CASE WHEN ci.ingredient_id IN (SELECT ingredient_id FROM user_ingredients WHERE user_id = ?) THEN 1 ELSE 0 END) = COUNT(*)', [$userId]) ->pluck('id'); + + // Programatically find cocktails that match your ingredients with possible substituted ingredients. + // This is currently probably not really performant + $userIngredients = $this->db->table('user_ingredients')->select('ingredient_id')->where('user_id', $userId)->pluck('ingredient_id'); // TODO: extract, and reuse + $possibleCocktailsWithSubstitutes = Cocktail::select('cocktails.*') + ->join('cocktail_ingredients AS ci', 'ci.cocktail_id', '=', 'cocktails.id') + ->join('cocktail_ingredient_substitutes AS cis', 'cis.cocktail_ingredient_id', '=', 'ci.id') + ->join('user_ingredients AS ui', 'ui.ingredient_id', '=', 'cis.ingredient_id') + ->where('ui.user_id', $userId) + ->get(); + + $subCocktails = []; + foreach ($possibleCocktailsWithSubstitutes as $cocktail) { + $ingredientsCount = 0; + foreach ($cocktail->ingredients as $cocktailIngredient) { + if ($userIngredients->contains($cocktailIngredient->ingredient_id)) { // User has original ingredient + $ingredientsCount++; + } elseif ($userIngredients->intersect($cocktailIngredient->substitutes->pluck('ingredient_id'))->count() > 0) { // User has one of the substitiute ingredients + $ingredientsCount++; + } + } + if ($ingredientsCount === $cocktail->ingredients->count()) { // User can make this cocktail + $subCocktails[] = $cocktail->id; + } + } - return Cocktail::find($cocktailIds); + return Cocktail::find(array_merge($cocktailIds->toArray(), $subCocktails)); } /** diff --git a/composer.json b/composer.json index 652389a1..43cf8222 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "symfony/yaml": "^6.1" }, "require-dev": { + "barryvdh/laravel-debugbar": "^3.7", "fakerphp/faker": "^1.9.1", "laravel/pint": "^1.0", "laravel/sail": "^1.0.1", diff --git a/composer.lock b/composer.lock index 8378d3ae..000f182b 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": "afaab1efcd8ad630be01f29320182851", + "content-hash": "20a4532bd1d53d97314ea5577d293c11", "packages": [ { "name": "brick/math", @@ -6300,6 +6300,90 @@ } ], "packages-dev": [ + { + "name": "barryvdh/laravel-debugbar", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "3372ed65e6d2039d663ed19aa699956f9d346271" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/3372ed65e6d2039d663ed19aa699956f9d346271", + "reference": "3372ed65e6d2039d663ed19aa699956f9d346271", + "shasum": "" + }, + "require": { + "illuminate/routing": "^7|^8|^9", + "illuminate/session": "^7|^8|^9", + "illuminate/support": "^7|^8|^9", + "maximebf/debugbar": "^1.17.2", + "php": ">=7.2.5", + "symfony/finder": "^5|^6" + }, + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/testbench-dusk": "^5|^6|^7", + "phpunit/phpunit": "^8.5|^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.6-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ], + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar" + } + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "laravel", + "profiler", + "webprofiler" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-debugbar/issues", + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2022-07-11T09:26:42+00:00" + }, { "name": "composer/class-map-generator", "version": "1.0.0", @@ -6898,6 +6982,72 @@ }, "time": "2022-08-18T16:18:26+00:00" }, + { + "name": "maximebf/debugbar", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/maximebf/php-debugbar.git", + "reference": "ba0af68dd4316834701ecb30a00ce9604ced3ee9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/ba0af68dd4316834701ecb30a00ce9604ced3ee9", + "reference": "ba0af68dd4316834701ecb30a00ce9604ced3ee9", + "shasum": "" + }, + "require": { + "php": "^7.1|^8", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^2.6|^3|^4|^5|^6" + }, + "require-dev": { + "phpunit/phpunit": "^7.5.20 || ^9.4.2", + "twig/twig": "^1.38|^2.7|^3.0" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + } + }, + "autoload": { + "psr-4": { + "DebugBar\\": "src/DebugBar/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/maximebf/php-debugbar", + "keywords": [ + "debug", + "debugbar" + ], + "support": { + "issues": "https://github.com/maximebf/php-debugbar/issues", + "source": "https://github.com/maximebf/php-debugbar/tree/v1.18.1" + }, + "time": "2022-03-31T14:55:54+00:00" + }, { "name": "mockery/mockery", "version": "1.5.1", diff --git a/config/bar-assistant.php b/config/bar-assistant.php index e6c800bd..5a87c5cc 100644 --- a/config/bar-assistant.php +++ b/config/bar-assistant.php @@ -11,7 +11,7 @@ | */ - 'version' => 'v0.4.0', + 'version' => 'v0.5.0', /* |-------------------------------------------------------------------------- diff --git a/docs/api.postman_collection.json b/docs/api.postman_collection.json index 1b353f2c..d5ebe4d1 100644 --- a/docs/api.postman_collection.json +++ b/docs/api.postman_collection.json @@ -170,6 +170,48 @@ } ] }, + { + "name": "Glasses", + "item": [ + { + "name": "All glasses", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/glasses", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "glasses" + ] + } + }, + "response": [] + }, + { + "name": "Show ingredient", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/glasses/1", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "glasses", + "1" + ] + } + }, + "response": [] + } + ] + }, { "name": "Ingredient Categories", "item": [ @@ -648,14 +690,13 @@ } }, "url": { - "raw": "{{base}}/api/shelf/batch", + "raw": "{{base}}/api/shelf", "host": [ "{{base}}" ], "path": [ "api", - "shelf", - "batch" + "shelf" ] } }, @@ -691,14 +732,14 @@ "method": "GET", "header": [], "url": { - "raw": "{{base}}/api/images/150", + "raw": "{{base}}/api/images/217", "host": [ "{{base}}" ], "path": [ "api", "images", - "150" + "217" ] } }, @@ -753,6 +794,41 @@ }, "response": [] }, + { + "name": "Partial image update", + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "copyright", + "value": "Copyright image 1", + "type": "text" + } + ] + }, + "url": { + "raw": "{{base}}/api/images/218", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "images", + "218" + ] + } + }, + "response": [] + }, { "name": "Delete image", "request": { @@ -890,120 +966,150 @@ ] }, { - "name": "Login", - "event": [ + "name": "Auth", + "item": [ { - "listen": "test", - "script": { - "exec": [ - "var resp = pm.response.json();\r", - "\r", - "pm.collectionVariables.set(\"token\", resp.token);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"email\": \"admin@example.com\",\r\n \"password\": \"password\"\r\n}", - "options": { - "raw": { - "language": "json" + "name": "Login", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var resp = pm.response.json();\r", + "\r", + "pm.collectionVariables.set(\"token\", resp.token);" + ], + "type": "text/javascript" + } } - } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"admin@example.com\",\r\n \"password\": \"password\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base}}/api/login", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "login" + ] + } + }, + "response": [] }, - "url": { - "raw": "{{base}}/api/login", - "host": [ - "{{base}}" + { + "name": "Logout", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } ], - "path": [ - "api", - "login" - ] - } - }, - "response": [] - }, - { - "name": "Logout", - "event": [ + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base}}/api/logout", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "logout" + ] + } + }, + "response": [] + }, { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [], - "url": { - "raw": "{{base}}/api/logout", - "host": [ - "{{base}}" + "name": "Register a new user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } ], - "path": [ - "api", - "logout" - ] + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"contact@karlomikus.com\",\r\n \"name\": \"Postman\",\r\n \"password\": \"12345\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base}}/api/register", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "register" + ] + } + }, + "response": [] } - }, - "response": [] + ] }, { - "name": "Register a new user", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], + "name": "Version", "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"email\": \"contact@karlomikus.com\",\r\n \"name\": \"Postman\",\r\n \"password\": \"12345\"\r\n}", - "options": { - "raw": { - "language": "json" - } + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" } - }, + ], "url": { - "raw": "{{base}}/api/register", + "raw": "{{base}}/api/server/version", "host": [ "{{base}}" ], "path": [ "api", - "register" + "server", + "version" ] } }, "response": [] }, { - "name": "Version", + "name": "Open Api Spec", "request": { "method": "GET", "header": [ @@ -1014,13 +1120,14 @@ } ], "url": { - "raw": "{{base}}/api/version", + "raw": "{{base}}/api/server/openapi", "host": [ "{{base}}" ], "path": [ "api", - "version" + "server", + "openapi" ] } }, diff --git a/docs/open-api-spec.yml b/docs/open-api-spec.yml new file mode 100644 index 00000000..6540c847 --- /dev/null +++ b/docs/open-api-spec.yml @@ -0,0 +1,1246 @@ +openapi: '3.0.2' +info: + title: 'Bar Assistant API' + version: '1.0.0' + description: |- + Bar assistant is a self hosted application for managing your home bar. It allows you to add ingredients and create custom cocktail recipes. +servers: + - url: /api +tags: + - name: Server + description: Operations related to server + - name: Auth + description: Operations related to user authentication + - name: Ingredients + description: Operations related to ingredients + - name: Cocktails + description: Operations related to cocktails + - name: Glasses + description: Operations related to glasses + - name: Images + description: Operations related to images + - name: "Ingredient categories" + description: Operations related to ingredient categories + - name: "User shelf" + description: Operations related to user shelf + - name: Users + description: Operations related to users + - name: "Shopping list" + description: Operations related to user shopping list +security: + - user_token: [] +paths: + /server/version: + get: + tags: + - Server + summary: Get server version information + security: [] + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Version' + /server/openapi: + get: + tags: + - Server + summary: Get open api specification in yaml format + security: [] + responses: + '200': + description: OpenAPI schema in yaml format + /login: + post: + tags: + - Auth + summary: Authenticate and get a token + security: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + type: string + example: admin@example.com + password: + type: string + example: password + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + token: + type: string + example: 1|dvWHLWuZbmWWFbjaUDla393Q9jK5Ou9ujWYPcvII + '422': + $ref: '#/components/responses/UnprocessableEntity' + /logout: + post: + tags: + - Auth + summary: Logout currently authenticated user + security: [] + responses: + '200': + description: Successful response + /cocktails: + parameters: + - in: query + name: user_id + required: false + schema: + type: integer + example: 1 + description: 'Show only cocktails made by a specifc user' + get: + tags: + - "Cocktails" + summary: 'Show a paginated list of cocktails' + responses: + '200': + description: Successful response + content: + application/json: + schema: + allOf: + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Cocktail' + - $ref: '#/components/schemas/PaginationResponse' + post: + tags: + - "Cocktails" + summary: 'Create a new cocktail' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CocktailRequest' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Cocktail' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /register: + post: + tags: + - Auth + security: [] + summary: "Register a new user" + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + type: string + example: "newuser@domain.com" + name: + type: string + example: "New User" + password: + type: string + example: "P4SSW0RD" + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/User' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /cocktails/{id}: + parameters: + - name: id + in: path + required: true + description: Database id of the cocktail + schema: + type: integer + get: + tags: + - "Cocktails" + summary: 'Details of a specific cocktail' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Cocktail' + put: + tags: + - "Cocktails" + summary: 'Update a specific cocktail' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CocktailRequest' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Cocktail' + '422': + $ref: '#/components/responses/UnprocessableEntity' + delete: + tags: + - "Cocktails" + summary: 'Delete a specific cocktail' + responses: + '200': + description: Successful response + /cocktails/{id}/toggle-favorite: + parameters: + - name: id + in: path + required: true + description: Database id of the cocktail + schema: + type: integer + post: + tags: + - "Cocktails" + summary: 'Toggle adding cocktail to favorites' + responses: + '200': + description: Successful response + /cocktails/random: + get: + tags: + - "Cocktails" + summary: 'Get a random cocktail' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Cocktail' + /cocktails/user-shelf: + get: + tags: + - "Cocktails" + summary: 'Get a list of cocktails that currently authorized user can make' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Cocktail' + /cocktails/user-favorites: + get: + tags: + - "Cocktails" + summary: 'Get a list of cocktails that currently authorized user added to his favorites' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Cocktail' + /ingredients: + get: + tags: + - Ingredients + summary: 'Get a list of ingredients' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Ingredient' + post: + tags: + - Ingredients + summary: 'Create a new ingredient' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IngredientRequest' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Ingredient' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /ingredients/{id}: + parameters: + - name: id + in: path + required: true + description: Database id of the ingredient + schema: + type: integer + get: + tags: + - Ingredients + summary: 'Details of a specific ingredient' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Ingredient' + '404': + $ref: '#/components/responses/NotFound' + put: + tags: + - Ingredients + summary: 'Update an existing ingredient' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IngredientRequest' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Ingredient' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '404': + $ref: '#/components/responses/NotFound' + delete: + tags: + - Ingredients + summary: 'Delete a specific ingredient' + responses: + '200': + description: Successful response + '404': + $ref: '#/components/responses/NotFound' + /glasses: + get: + tags: + - Glasses + summary: Show a list of glass types + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Glass' + /glasses/{id}: + get: + tags: + - Glasses + summary: Get a specific glass type + parameters: + - in: path + name: id + schema: + type: integer + required: true + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Glass' + '404': + $ref: '#/components/responses/NotFound' + /images: + post: + tags: + - Images + summary: Save multiple images + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ImageRequest' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Image' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /images/{id}: + parameters: + - in: path + name: id + description: 'Database id of the image' + schema: + type: integer + required: true + get: + tags: + - Images + summary: Get a specific image + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Image' + '404': + $ref: '#/components/responses/NotFound' + put: + tags: + - Images + summary: Update image copyright + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + copyright: + type: string + example: 'Copyright of the image' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Image' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '404': + $ref: '#/components/responses/NotFound' + delete: + tags: + - Images + summary: Remove a specific image + responses: + '200': + description: Successful response + /ingredient-categories: + get: + tags: + - "Ingredient categories" + summary: 'Show a list of ingredient categories' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/IngredientCategory' + post: + tags: + - "Ingredient categories" + summary: 'Create a new ingredient category' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IngredientCategoryRequest' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/IngredientCategory' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /ingredient-categories/{id}: + parameters: + - in: path + name: id + description: 'Database id of the ingredient category' + schema: + type: integer + required: true + get: + tags: + - "Ingredient categories" + summary: Get a specific ingredient category + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/IngredientCategory' + put: + tags: + - "Ingredient categories" + summary: Update a specific ingredient category + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IngredientCategoryRequest' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/IngredientCategory' + '422': + $ref: '#/components/responses/UnprocessableEntity' + delete: + tags: + - "Ingredient categories" + summary: Delete specific ingredient category + responses: + '200': + description: Successful response + /shelf: + get: + tags: + - "User shelf" + summary: "Get all ingredients in user shelf" + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/UserIngredient' + post: + tags: + - "User shelf" + summary: "Add multiple ingredients to user shelf" + requestBody: + content: + application/json: + schema: + type: object + properties: + ingredient_ids: + type: array + example: [1] + items: + type: integer + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/UserIngredient' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /shelf/{ingredient_id}: + parameters: + - in: path + name: ingredient_id + description: 'Database id of the ingredient' + schema: + type: integer + required: true + post: + tags: + - "User shelf" + summary: "Add a single ingredient to user shelf" + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/UserIngredient' + '422': + $ref: '#/components/responses/UnprocessableEntity' + delete: + tags: + - "User shelf" + summary: Delete a single ingredient from user shelf + responses: + '200': + description: Successful response + /user: + get: + tags: + - "Users" + summary: "Get info about currently authenticated user" + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/User' + /shopping-lists/batch: + post: + tags: + - "Shopping list" + summary: "Add ingredients to a shopping list" + requestBody: + content: + application/json: + schema: + type: object + properties: + ingredient_ids: + type: array + example: [1, 2, 3] + items: + type: integer + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/UserShoppingList' + '422': + $ref: '#/components/responses/UnprocessableEntity' + delete: + tags: + - "Shopping list" + summary: "Delete ingredients from a shopping list" + requestBody: + content: + application/json: + schema: + type: object + properties: + ingredient_ids: + type: array + example: [1, 2, 3] + items: + type: integer + responses: + '200': + description: Successful response +components: + securitySchemes: + user_token: + type: http + scheme: bearer + responses: + UnprocessableEntity: + description: Request body validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationErrorResponse' + NotFound: + description: Resource not found + content: + application/json: + schema: + type: object + properties: + type: + type: string + example: api_error + message: + type: string + example: 'Resource record not found.' + schemas: + Glass: + type: object + properties: + id: + type: integer + example: 1 + readOnly: true + name: + type: string + example: 'Cocktail glass' + description: + type: string + nullable: true + example: 'Description of glass' + IngredientRequest: + type: object + properties: + name: + type: string + example: 'Jack Daniels' + strength: + type: number + format: float + example: 40.0 + description: + type: string + example: 'A type of whiskey' + nullable: true + origin: + type: string + example: 'North America' + nullable: true + images: + type: array + example: [1, 2, 3] + items: + type: integer + ingredient_category_id: + type: integer + example: 1 + color: + type: string + format: hex + example: '#ffffff' + nullable: true + parent_ingredient_id: + type: integer + example: 1 + nullable: true + Ingredient: + type: object + properties: + id: + type: integer + example: 1 + readOnly: true + slug: + type: string + example: 'jack-daniels' + readOnly: true + name: + type: string + example: 'Jack Daniels' + strength: + type: number + format: float + example: 40.0 + description: + type: string + example: 'A type of whiskey' + origin: + type: string + example: 'North America' + main_image_id: + type: integer + example: 1 + nullable: true + images: + type: array + example: [1, 2, 3] + items: + type: integer + ingredient_category_id: + type: integer + example: 1 + color: + type: string + format: hex + example: '#ffffff' + nullable: true + category: + $ref: '#/components/schemas/IngredientCategory' + cocktails_count: + type: integer + example: 12 + varieties: + type: array + items: + type: object + properties: + id: + type: integer + example: 1 + slug: + type: string + example: 'ingredient-1' + name: + type: string + example: 'Ingredient 1' + cocktails: + type: array + items: + type: object + properties: + id: + type: integer + example: 1 + slug: + type: string + example: 'cocktail-1' + name: + type: string + example: 'Cocktail 1' + IngredientCategory: + type: object + properties: + id: + type: integer + example: 1 + readOnly: true + name: + type: string + example: Spirits + description: + type: string + nullable: true + example: 'A category of base spirits' + IngredientCategoryRequest: + type: object + properties: + name: + type: string + example: Spirits + description: + type: string + nullable: true + example: 'A category of base spirits' + Version: + type: object + properties: + name: + type: string + example: 'Bar Assistant' + version: + type: string + example: 'v1.0.0' + meilisearch_host: + type: string + format: hostname + example: 'https://my-meilisearch-server.com' + meilisearch_version: + type: string + example: '0.29.0' + Image: + type: object + properties: + id: + type: integer + example: 1 + file_path: + type: string + example: 'ingredients/ingredient-image.png' + url: + type: string + example: 'http://localhost.com/app_images/ingredients/ingredient-image.png' + copyright: + type: string + example: 'Somewhere from the web' + last_modified: + type: string + example: '2022-11-17T09:52:48.000000Z' + User: + type: object + properties: + id: + type: integer + example: 1 + name: + type: string + example: 'Bar Tender' + email: + type: string + example: 'bar@tender.com' + search_host: + type: string + example: 'http://meilisearch-server.com' + search_api_key: + type: string + example: MEILI_API_KEY + favorite_cocktails: + type: array + example: [1, 2] + items: + type: integer + shelf_ingredients: + type: array + example: [1, 2] + items: + type: integer + shopping_lists: + type: array + example: [1, 2] + items: + type: integer + CocktailIngredientSubstitute: + type: object + properties: + id: + type: integer + example: 1 + slug: + type: string + example: 'ingredient-1' + name: + type: string + example: 'Ingredient 1' + CocktailIngredient: + type: object + properties: + id: + type: integer + example: 1 + sort: + type: integer + example: 0 + amount: + type: number + format: float + example: 30.0 + units: + type: string + example: ml + optional: + type: boolean + example: false + ingredient_id: + type: integer + example: 1 + name: + type: string + example: 'Ingredient name' + ingredient_slug: + type: string + example: 'ingredient-name' + substitutes: + type: array + items: + $ref: '#/components/schemas/CocktailIngredientSubstitute' + Cocktail: + type: object + properties: + id: + type: integer + example: 1 + readOnly: true + slug: + type: string + example: 'cocktail-name' + readOnly: true + name: + type: string + example: 'Cocktail name' + instructions: + type: string + example: |- + 1. Step + 2. Step + garnish: + type: string + nullable: true + example: Lemon wheel + description: + type: string + nullable: true + example: 'A short cocktail description' + source: + type: string + nullable: true + example: http://wikipedia.org + main_image_id: + type: integer + example: 1 + nullable: true + images: + type: array + items: + $ref: '#/components/schemas/Image' + tags: + type: array + example: ['Gin', 'IBA Official'] + items: + type: string + user_id: + type: integer + example: 1 + glass: + $ref: '#/components/schemas/Glass' + short_ingredients: + type: array + example: ['Gin', 'Tonic', 'Lemon Juice'] + items: + type: string + ingredients: + type: array + items: + $ref: '#/components/schemas/CocktailIngredient' + CocktailRequest: + type: object + required: + - name + - instructions + properties: + name: + type: string + example: 'Cocktail name' + instructions: + type: string + example: |- + 1. Step + 2. Step + garnish: + type: string + nullable: true + example: Lemon wheel + description: + type: string + nullable: true + example: 'A short cocktail description' + source: + type: string + nullable: true + example: http://wikipedia.org + images: + type: array + example: [1, 2] + items: + type: integer + description: Image resource id + example: 1 + tags: + type: array + example: ['Gin', 'IBA Official'] + items: + type: string + glass_id: + type: integer + example: 1 + nullable: true + ingredients: + type: array + items: + type: object + properties: + ingredient_id: + type: integer + example: 1 + amount: + type: number + format: float + example: 30 + units: + type: string + example: ml + optional: + type: boolean + example: false + sort: + type: integer + example: 0 + ImageRequest: + type: object + properties: + images: + type: array + items: + type: object + properties: + image: + type: string + format: binary + copyright: + type: string + UserIngredient: + type: object + properties: + id: + type: integer + example: 1 + user_id: + type: integer + example: 1 + ingredient_id: + type: integer + example: 1 + UserShoppingList: + type: object + properties: + id: + type: integer + example: 1 + user_id: + type: integer + example: 1 + ingredient_id: + type: integer + example: 1 + ValidationErrorResponse: + type: object + properties: + message: + type: string + example: "The cocktail name must be a string. (and 4 more errors)" + errors: + type: object + properties: + property_key: + type: array + example: ["The property_key field is required."] + items: + type: string + PaginationResponse: + type: object + properties: + links: + type: object + properties: + first: + type: string + example: http://localhost/api/resource?page=1 + last: + type: string + example: http://localhost/api/resource?page=10 + prev: + type: string + nullable: true + example: null + next: + type: string + nullable: true + example: http://localhost/api/resource?page=2 + meta: + type: object + properties: + current_page: + type: integer + example: 1 + from: + type: integer + example: 1 + last_page: + type: integer + example: 10 + links: + type: array + items: + type: object + properties: + url: + type: string + label: + type: string + active: + type: boolean + path: + type: string + example: http://localhost/api/resource + per_page: + type: integer + example: 15 + to: + type: integer + example: 15 + total: + type: integer + example: 100 diff --git a/docs/spec.yml b/docs/spec.yml deleted file mode 100644 index 9568bf53..00000000 --- a/docs/spec.yml +++ /dev/null @@ -1,595 +0,0 @@ -openapi: '3.0.2' -info: - title: 'Bar Assistant API' - version: '1.0.0' - description: |- - Bar assistant is a self hosted application for managing your home bar. It allows you to add ingredients and create custom cocktail recipes. -servers: - - url: https://127.0.0.1:8000/api -tags: - - name: ingredients - description: Operations related to ingredients - - name: cocktails - description: Operations related to cocktails - - name: glasses - description: Operations related to glasses - - name: health - description: Operations related to server -security: - - user_token: [] -paths: - /version: - get: - tags: - - health - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/Version' - /cocktails: - parameters: - - in: query - name: user_id - required: false - schema: - type: integer - example: 1 - description: 'Show only cocktails made by a specifc user' - get: - tags: - - cocktails - summary: 'Show a paginated list of cocktails' - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/Cocktail' - post: - tags: - - cocktails - summary: 'Create a new cocktail' - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/Cocktail' - /cocktails/{id}: - parameters: - - name: id - in: path - required: true - description: Database id of the cocktail - schema: - type: integer - get: - tags: - - cocktails - summary: 'Details of a specific cocktail' - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - data: - $ref: '#/components/schemas/Cocktail' - put: - tags: - - cocktails - summary: 'Update a specific cocktail' - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - data: - $ref: '#/components/schemas/Cocktail' - delete: - tags: - - cocktails - summary: 'Delete a specific cocktail' - responses: - '200': - description: OK - /cocktails/random: - get: - tags: - - cocktails - summary: 'Get a random cocktail' - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - data: - $ref: '#/components/schemas/Cocktail' - /cocktails/user-shelf: - get: - tags: - - cocktails - summary: 'Get a list of cocktails that currently authorized user can make' - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/Cocktail' - /ingredients: - get: - tags: - - ingredients - summary: 'Get a list of ingredients' - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/Ingredient' - post: - tags: - - ingredients - summary: 'Create a new ingredient' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/IngredientRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - data: - $ref: '#/components/schemas/Ingredient' - /ingredients/{id}: - parameters: - - name: id - in: path - required: true - description: Database id of the ingredient - schema: - type: integer - get: - tags: - - ingredients - summary: 'Details of a specific ingredient' - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - data: - $ref: '#/components/schemas/Ingredient' - put: - tags: - - ingredients - summary: 'Update an existing ingredient' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/IngredientRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - data: - $ref: '#/components/schemas/Ingredient' - delete: - tags: - - ingredients - summary: 'Delete a specific ingredient' - responses: - '200': - description: OK - /glasses: - get: - tags: - - glasses - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/Glass' - /glasses/{id}: - get: - tags: - - glasses - parameters: - - in: path - name: id - schema: - type: integer - required: true - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - data: - $ref: '#/components/schemas/Glass' -components: - securitySchemes: - user_token: - type: http - scheme: bearer - schemas: - Glass: - type: object - properties: - id: - type: integer - example: 1 - readOnly: true - name: - type: string - example: 'Cocktail glass' - description: - type: string - nullable: true - example: 'Description of glass' - IngredientRequest: - type: object - properties: - name: - type: string - example: 'Jack Daniels' - strength: - type: number - format: float - example: 40.0 - description: - type: string - example: 'A type of whiskey' - nullable: true - origin: - type: string - example: 'North America' - nullable: true - images: - type: array - example: [1, 2, 3] - items: - type: integer - ingredient_category_id: - type: integer - example: 1 - color: - type: string - format: hex - example: '#ffffff' - nullable: true - parent_ingredient_id: - type: integer - example: 1 - nullable: true - Ingredient: - type: object - properties: - id: - type: integer - example: 1 - readOnly: true - slug: - type: string - example: 'jack-daniels' - readOnly: true - name: - type: string - example: 'Jack Daniels' - strength: - type: number - format: float - example: 40.0 - description: - type: string - example: 'A type of whiskey' - origin: - type: string - example: 'North America' - main_image_id: - type: integer - example: 1 - nullable: true - images: - type: array - example: [1, 2, 3] - items: - type: integer - ingredient_category_id: - type: integer - example: 1 - color: - type: string - format: hex - example: '#ffffff' - nullable: true - category: - $ref: '#/components/schemas/IngredientCategory' - cocktails_count: - type: integer - example: 12 - varieties: - type: array - items: - type: object - properties: - id: - type: integer - example: 1 - slug: - type: string - example: 'ingredient-1' - name: - type: string - example: 'Ingredient 1' - cocktails: - type: array - items: - type: object - properties: - id: - type: integer - example: 1 - slug: - type: string - example: 'cocktail-1' - name: - type: string - example: 'Cocktail 1' - IngredientCategory: - type: object - properties: - id: - type: integer - example: 1 - readOnly: true - name: - type: string - example: Spirits - description: - type: string - nullable: true - example: 'A category of base spirits' - Version: - type: object - properties: - name: - type: string - example: 'Bar Assistant' - version: - type: string - example: 'v1.0.0' - meilisearch_host: - type: string - format: hostname - example: 'https://my-meilisearch-server.com' - meilisearch_version: - type: string - example: '0.29.0' - Image: - type: object - properties: - id: - type: integer - example: 1 - file_path: - type: string - example: 'ingredients/ingredient-image.png' - url: - type: string - example: 'http://localhost.com/app_images/ingredients/ingredient-image.png' - copyright: - type: string - example: 'Somewhere from the web' - last_modified: - type: string - example: '2022-11-17T09:52:48.000000Z' - User: - type: object - properties: - id: - type: integer - example: 1 - name: - type: string - example: 'Bar Tender' - email: - type: string - example: 'bar@tender.com' - search_host: - type: string - example: 'http://meilisearch-server.com' - search_api_key: - type: string - example: MEILI_API_KEY - favorite_cocktails: - type: array - example: [1, 2] - items: - type: integer - shelf_ingredients: - type: array - example: [1, 2] - items: - type: integer - shopping_lists: - type: array - example: [1, 2] - items: - type: integer - CocktailIngredientSubstitute: - type: object - properties: - id: - type: integer - example: 1 - slug: - type: string - example: 'ingredient-1' - name: - type: string - example: 'Ingredient 1' - CocktailIngredient: - type: object - properties: - id: - type: integer - example: 1 - sort: - type: integer - example: 0 - amount: - type: number - format: float - example: 30.0 - units: - type: string - example: ml - optional: - type: boolean - example: false - ingredient_id: - type: integer - example: 1 - name: - type: string - example: 'Ingredient name' - ingredient_slug: - type: string - example: 'ingredient-name' - substitutes: - type: array - items: - $ref: '#/components/schemas/CocktailIngredientSubstitute' - Cocktail: - type: object - properties: - id: - type: integer - example: 1 - readOnly: true - slug: - type: string - example: 'cocktail-name' - readOnly: true - name: - type: string - example: 'Cocktail name' - instructions: - type: string - nullable: true - example: |- - 1. Step - 2. Step - garnish: - type: string - nullable: true - example: Lemon wheel - description: - type: string - nullable: true - example: 'A short cocktail description' - source: - type: string - nullable: true - example: http://wikipedia.org - main_image_id: - type: integer - example: 1 - nullable: true - images: - type: array - items: - $ref: '#/components/schemas/Image' - tags: - type: array - example: ['Gin', 'IBA Official'] - items: - type: string - user_id: - type: integer - example: 1 - glass: - $ref: '#/components/schemas/Glass' - short_ingredients: - type: array - example: ['Gin', 'Tonic', 'Lemon Juice'] - items: - type: string - ingredients: - type: array - items: - $ref: '#/components/schemas/CocktailIngredient' - ErrorResponse: - type: object - properties: - type: - type: string - example: 'api_error' - message: - type: string - example: 'This is an detailed error message' - SuccessActionResponse: - type: object - properties: - success: - type: boolean - example: true diff --git a/resources/data/iba_cocktails_v0.1.0.yml b/resources/data/iba_cocktails_v0.1.0.yml index 7c2c6fc3..d91b02f3 100644 --- a/resources/data/iba_cocktails_v0.1.0.yml +++ b/resources/data/iba_cocktails_v0.1.0.yml @@ -402,8 +402,9 @@ - amount: 45 units: ml - name: Whiskey + name: 'Bourbon Whiskey' optional: false + substitutes: ['Rye Whiskey'] - amount: 30 units: ml @@ -585,6 +586,7 @@ units: ml name: Gin optional: false + substitutes: ['Old Tom Gin'] - amount: 10 units: ml @@ -632,7 +634,7 @@ amount: 1 units: drops name: 'Grand Marnier' - optional: false + optional: true - amount: 1 units: cube @@ -736,6 +738,7 @@ units: ml name: Vodka optional: false + substitutes: ['Vodka Citron'] - amount: 15 units: ml @@ -1403,6 +1406,7 @@ units: ml name: Vodka optional: false + substitutes: ['Vodka Citron'] - amount: 20 units: ml diff --git a/resources/data/popular_cocktails.yml b/resources/data/popular_cocktails.yml index 85ff81ed..a11be38d 100644 --- a/resources/data/popular_cocktails.yml +++ b/resources/data/popular_cocktails.yml @@ -503,8 +503,9 @@ - amount: 25 units: ml - name: 'Elderflower Cordial' + name: 'St-Germain' optional: false + substitutes: ['Elderflower Cordial'] - amount: 7.5 units: ml @@ -578,7 +579,7 @@ instructions: 'Shake all ingredients with ice and strain into chilled glass.' garnish: 'Lime zest' source: 'https://www.cocktailexplorer.co/cocktails/queens-park-hotel-super-cocktail/anders-erickson/' - image_copyright: null + image_copyright: Anders Erickson tags: - Rum glass: Coupe @@ -608,3 +609,76 @@ units: dashes name: 'Angostura aromatic bitters' optional: false +- + name: Seelbach + description: "The Seelbach Cocktail may not be a julep, but it doesn't have to be: it's respectable, powerful, and delicious right down to the bottom of the glass." + instructions: |- + 1. Add the bourbon, Cointreau, Angostura bitters and Peychaudā€™s bitters into a mixing glass with ice and stir until well-chilled. + 2. Strain into a chilled flute. + 3. Top with cold Champagne or other sparkling wine. + 3. Garnish with an orange twist. + garnish: 'Orange twist' + source: 'https://www.seriouseats.com/seelbach-cocktail-recipe' + image_copyright: The Educated Barfly + tags: + - Whiskey + glass: Champagne + ingredients: + - + amount: 30 + units: ml + name: 'Bourbon Whiskey' + optional: false + - + amount: 15 + units: ml + name: 'Cointreau' + optional: false + - + amount: 5 + units: dashes + name: 'Angostura aromatic bitters' + optional: false + - + amount: 5 + units: dashes + name: 'Peychauds Bitters' + optional: false + - + amount: 90 + units: ml + name: Champagne + optional: false +- + name: El Presidente + description: The El Presidente earned its acclaim in Havana during the 1920s through the 1940s during the American Prohibition. It quickly became the preferred drink of the Cuban upper class. + instructions: |- + Add all ingredients to a mixing glass with ice and stir until well-chilled. + garnish: null + source: 'https://www.liquor.com/recipes/el-presidente/' + image_copyright: A Couple Cooks + tags: + - Rum + glass: Cocktail + ingredients: + - + amount: 45 + units: ml + name: 'White rum' + optional: false + - + amount: 22.5 + units: ml + name: 'Dry Vermouth' + optional: false + substitutes: ['Lillet Blanc'] + - + amount: 7.5 + units: ml + name: 'Orange CuraƧao' + optional: false + - + amount: 1 + units: teaspoon + name: 'Grenadine Syrup' + optional: false diff --git a/resources/entrypoint.sh b/resources/entrypoint.sh index ed3bcc62..60e2db75 100644 --- a/resources/entrypoint.sh +++ b/resources/entrypoint.sh @@ -26,9 +26,9 @@ system_start_checkup() { php artisan config:cache php artisan route:cache - echo "Setting permissions..." + # echo "Setting permissions..." - chown -R www-data:www-data /var/www/cocktails + # chown -R www-data:www-data /var/www/cocktails echo "Application ready!" fi diff --git a/resources/views/swagger.blade.php b/resources/views/swagger.blade.php new file mode 100644 index 00000000..bdae95fc --- /dev/null +++ b/resources/views/swagger.blade.php @@ -0,0 +1,37 @@ + + + + + Docs + + + + +
+ Loading.... +
+ + + + + diff --git a/routes/api.php b/routes/api.php index 160c6e8b..8b4fbd32 100644 --- a/routes/api.php +++ b/routes/api.php @@ -6,7 +6,7 @@ use Kami\Cocktail\Http\Controllers\GlassController; use Kami\Cocktail\Http\Controllers\ImageController; use Kami\Cocktail\Http\Controllers\ShelfController; -use Kami\Cocktail\Http\Controllers\HealthController; +use Kami\Cocktail\Http\Controllers\ServerController; use Kami\Cocktail\Http\Controllers\CocktailController; use Kami\Cocktail\Http\Controllers\IngredientController; use Kami\Cocktail\Http\Controllers\ShoppingListController; @@ -23,14 +23,19 @@ | */ -Route::post('login', [AuthController::class, 'authenticate']); +Route::get('/', [ServerController::class, 'index']); + +Route::post('login', [AuthController::class, 'authenticate'])->name('auth.login'); Route::post('register', [AuthController::class, 'register']); -Route::get('/version', [HealthController::class, 'version']); +Route::prefix('server')->group(function() { + Route::get('/version', [ServerController::class, 'version']); + Route::get('/openapi', [ServerController::class, 'openApi']); +}); Route::middleware('auth:sanctum')->group(function() { - Route::post('logout', [AuthController::class, 'logout']); + Route::post('logout', [AuthController::class, 'logout'])->name('auth.logout'); Route::get('/user', [UserController::class, 'show']); diff --git a/routes/web.php b/routes/web.php index d151402c..612d085b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,5 +14,9 @@ */ Route::get('/', function () { - return 'This is your Bar Assistant instance'; + return 'This is your Bar Assistant instance. Checkout /docs to see documentation.'; +}); + +Route::get('/docs', function () { + return view('swagger'); }); diff --git a/resources/data/images/.gitignore b/storage/debugbar/.gitignore similarity index 100% rename from resources/data/images/.gitignore rename to storage/debugbar/.gitignore diff --git a/storage/uploads/cocktails/a-new-cocktail.png b/storage/uploads/cocktails/a-new-cocktail.png deleted file mode 100644 index 4672ad9e..00000000 Binary files a/storage/uploads/cocktails/a-new-cocktail.png and /dev/null differ diff --git a/storage/uploads/cocktails/el-presidente.jpg b/storage/uploads/cocktails/el-presidente.jpg new file mode 100644 index 00000000..9ac63676 Binary files /dev/null and b/storage/uploads/cocktails/el-presidente.jpg differ diff --git a/storage/uploads/cocktails/seelbach.jpg b/storage/uploads/cocktails/seelbach.jpg new file mode 100644 index 00000000..e174012d Binary files /dev/null and b/storage/uploads/cocktails/seelbach.jpg differ diff --git a/storage/uploads/ingredients/st-germain.png b/storage/uploads/ingredients/st-germain.png new file mode 100644 index 00000000..12e37abb Binary files /dev/null and b/storage/uploads/ingredients/st-germain.png differ diff --git a/storage/uploads/ingredients/vodka-citron.png b/storage/uploads/ingredients/vodka-citron.png new file mode 100644 index 00000000..350c63d0 Binary files /dev/null and b/storage/uploads/ingredients/vodka-citron.png differ diff --git a/tests/Feature/HealthControllerTest.php b/tests/Feature/HealthControllerTest.php index 7301645a..3aa7846a 100644 --- a/tests/Feature/HealthControllerTest.php +++ b/tests/Feature/HealthControllerTest.php @@ -9,7 +9,7 @@ class HealthControllerTest extends TestCase { public function test_version_response() { - $response = $this->getJson('/api/version'); + $response = $this->getJson('/api/server/version'); $response->assertStatus(200);