├── public
├── favicon.ico
├── robots.txt
├── vendor
│ └── horizon
│ │ ├── favicon.png
│ │ ├── img
│ │ └── favicon.png
│ │ ├── mix-manifest.json
│ │ └── manifest.json
└── .htaccess
├── database
├── .gitignore
├── seeders
│ └── DatabaseSeeder.php
├── factories
│ ├── NoteFactory.php
│ ├── TagFactory.php
│ ├── RatingFactory.php
│ ├── BarIngredientFactory.php
│ ├── GlassFactory.php
│ ├── UtensilFactory.php
│ ├── CalculatorFactory.php
│ ├── UserIngredientFactory.php
│ ├── CocktailFavoriteFactory.php
│ ├── CollectionFactory.php
│ ├── ComplexIngredientFactory.php
│ ├── IngredientCategoryFactory.php
│ ├── CocktailMethodFactory.php
│ ├── BarFactory.php
│ ├── MenuFactory.php
│ ├── ImageFactory.php
│ ├── PriceCategoryFactory.php
│ ├── UserShoppingListFactory.php
│ ├── PersonalAccessTokenFactory.php
│ ├── BarMembershipFactory.php
│ ├── MenuCocktailFactory.php
│ ├── ExportFactory.php
│ ├── MenuIngredientFactory.php
│ ├── CocktailIngredientSubstituteFactory.php
│ ├── IngredientFactory.php
│ ├── CocktailIngredientFactory.php
│ ├── CalculatorBlockFactory.php
│ └── IngredientPriceFactory.php
└── migrations
│ ├── 2024_05_11_152732_add_settings_col_to_bars.php
│ ├── 2025_03_05_152429_add_settings_column_to_users.php
│ ├── 2025_07_05_134338_add_year_column_to_cocktails.php
│ ├── 2024_08_11_181521_add_meilisearch_token_to_bars.php
│ ├── 2024_08_17_120824_add_timestamps_to_price_categories.php
│ ├── 2025_07_25_201245_add_is_public_column_to_bars.php
│ ├── 2025_03_28_182053_add_default_units_to_ingredients.php
│ ├── 2025_03_02_201313_add_images_index.php
│ ├── 2023_10_11_210049_add_share_to_collections_table.php
│ ├── 2024_08_10_172139_add_quantity_to_shopping_list.php
│ ├── 2024_01_01_145353_add_parent_sub_column.php
│ ├── 2025_03_17_145628_add_cocktail_tag_index.php
│ ├── 2023_11_22_180057_mark_all_accounts_verified.php
│ ├── 2025_05_18_163451_add_complex_ingredients_index.php
│ ├── 2025_07_05_080957_add_parent_id_column_to_cocktails.php
│ ├── 2024_11_09_133632_migrate_bar_shelf_data.php
│ ├── 2024_06_01_135855_create_complex_ingredients_table.php
│ ├── 2024_04_01_140655_create_exports_table.php
│ ├── 2024_07_07_184922_create_price_categories_table.php
│ ├── 2024_10_26_145249_create_bar_shelf.php
│ ├── 2024_05_18_170708_add_total_volume_col_to_glasses.php
│ ├── 2025_02_26_161635_create_oauth_credentials_table.php
│ ├── 2025_03_01_154238_add_extra_ingredient_columns.php
│ ├── 2024_08_17_123305_add_nested_set.php
│ ├── 2019_12_14_000001_create_personal_access_tokens_table.php
│ ├── 2025_02_22_100015_create_menu_ingredients.php
│ └── 2024_07_07_185728_create_ingredient_prices_table.php
├── .spectral.yml
├── bootstrap
└── cache
│ └── .gitignore
├── storage
├── logs
│ └── .gitignore
├── app
│ ├── public
│ │ └── .gitignore
│ └── .gitignore
├── debugbar
│ └── .gitignore
├── http_cache
│ └── .gitignore
├── framework
│ ├── testing
│ │ └── .gitignore
│ ├── views
│ │ └── .gitignore
│ ├── cache
│ │ ├── data
│ │ │ └── .gitignore
│ │ └── .gitignore
│ ├── sessions
│ │ └── .gitignore
│ └── .gitignore
└── .gitignore
├── tests
├── fixtures
│ ├── sus_image.txt
│ ├── test.jpg
│ ├── cocktail.jpg
│ ├── datapack
│ │ ├── _meta.json
│ │ ├── base_utensils.json
│ │ ├── cocktails
│ │ │ └── test-cocktail
│ │ │ │ ├── c-1-img.jpg
│ │ │ │ └── data.json
│ │ ├── ingredients
│ │ │ └── test-ingredient
│ │ │ │ ├── i-1-img.png
│ │ │ │ └── data.json
│ │ ├── base_methods.json
│ │ ├── base_glasses.json
│ │ └── base_price_categories.json
│ ├── ingredients.csv
│ ├── external
│ │ ├── markdown.md
│ │ ├── json-ld.json
│ │ └── recipe.yml
│ └── import_collection.json
├── CreatesApplication.php
└── Feature
│ └── Http
│ └── ServerControllerTest.php
├── resources
├── art
│ ├── art1.png
│ ├── readme-logo.png
│ ├── readme-header.png
│ └── logo.svg
├── docker
│ ├── dist
│ │ ├── php.ini
│ │ └── nginx.conf
│ └── localdev
│ │ ├── prometheus.yml
│ │ └── php.ini
└── views
│ ├── emails
│ ├── test.blade.php
│ ├── account-deleted.blade.php
│ ├── password-changed.blade.php
│ ├── confirm-account.blade.php
│ ├── password-reset.blade.php
│ ├── subscription-changed.blade.php
│ ├── master.blade.php
│ └── subscribed.blade.php
│ ├── md_shopping_list_template.blade.php
│ ├── vendor
│ └── mail
│ │ └── html
│ │ ├── footer.blade.php
│ │ └── header.blade.php
│ ├── elements.blade.php
│ └── swagger.blade.php
├── phpstan.neon
├── pint.json
├── app
├── External
│ ├── SupportsXML.php
│ ├── SupportsYAML.php
│ ├── SupportsJSONLD.php
│ ├── SupportsMarkdown.php
│ ├── SupportsCSV.php
│ ├── BarOptionsEnum.php
│ ├── ForceUnitConvertEnum.php
│ ├── Import
│ │ └── DuplicateActionsEnum.php
│ ├── SupportsDraft2.php
│ ├── SupportsDataPack.php
│ └── ExportTypeEnum.php
├── Models
│ ├── BarType.php
│ ├── UserRole.php
│ ├── Enums
│ │ ├── BarTypeEnum.php
│ │ ├── UserRoleEnum.php
│ │ ├── CalculatorBlockTypeEnum.php
│ │ ├── MenuItemTypeEnum.php
│ │ ├── BarStatusEnum.php
│ │ └── AbilityEnum.php
│ ├── UploadableInterface.php
│ ├── Concerns
│ │ ├── IsExternalized.php
│ │ ├── HasAuthors.php
│ │ └── HasBarAwareScope.php
│ ├── CocktailPrice.php
│ ├── ValueObjects
│ │ ├── CalculatorResult.php
│ │ ├── SSOProvider.php
│ │ ├── CocktailIngredientFormatter.php
│ │ └── Price.php
│ ├── Note.php
│ ├── Rating.php
│ ├── PersonalAccessToken.php
│ ├── UserShoppingList.php
│ ├── CocktailFavorite.php
│ ├── BarIngredient.php
│ ├── FileToken.php
│ ├── UserIngredient.php
│ ├── PriceCategory.php
│ ├── Utensil.php
│ ├── ComplexIngredient.php
│ ├── CocktailMethod.php
│ ├── Collection.php
│ └── Tag.php
├── Exceptions
│ ├── IngredientMoveException.php
│ ├── ScraperMissingException.php
│ ├── ImageFileNotFoundException.php
│ ├── ImagesNotAttachedException.php
│ ├── IngredientParentException.php
│ ├── ExportFileNotCreatedException.php
│ ├── IngredientValidationException.php
│ └── IngredientPathTooDeepException.php
├── OpenAPI
│ ├── Schemas
│ │ ├── TagRequest.php
│ │ ├── APIError.php
│ │ ├── UtensilRequest.php
│ │ ├── CocktailMethodRequest.php
│ │ ├── NoteRequest.php
│ │ ├── IngredientRecommend.php
│ │ ├── ExportRequest.php
│ │ ├── PriceCategoryRequest.php
│ │ ├── LoginRequest.php
│ │ ├── ShoppingListRequest.php
│ │ ├── ValidationError.php
│ │ ├── UserRequest.php
│ │ ├── CollectionRequest.php
│ │ ├── ProfileSettings.php
│ │ ├── FileDownloadLink.php
│ │ ├── PersonalAccessTokenRequest.php
│ │ ├── BarSettings.php
│ │ ├── ImageRequest.php
│ │ ├── CalculatorBlockSettings.php
│ │ ├── CalculatorSolveRequest.php
│ │ ├── IngredientHierarchy.php
│ │ └── CalculatorRequest.php
│ ├── Parameters
│ │ ├── PageParameter.php
│ │ ├── PerPageParameter.php
│ │ ├── DatabaseIdParameter.php
│ │ └── BarIdHeaderParameter.php
│ ├── WrapObjectWithData.php
│ ├── WrapItemsWithData.php
│ ├── RateLimitResponse.php
│ ├── ValidationFailedResponse.php
│ ├── NotFoundResponse.php
│ └── NotAuthorizedResponse.php
├── Http
│ ├── Middleware
│ │ ├── EncryptCookies.php
│ │ ├── VerifyCsrfToken.php
│ │ ├── Authenticate.php
│ │ ├── PreventRequestsDuringMaintenance.php
│ │ ├── TrustHosts.php
│ │ ├── TrimStrings.php
│ │ ├── ValidateSignature.php
│ │ ├── TrustProxies.php
│ │ ├── CheckUserSubscription.php
│ │ └── RedirectIfAuthenticated.php
│ ├── Controllers
│ │ ├── Controller.php
│ │ ├── MetricsController.php
│ │ └── FeedsController.php
│ ├── Requests
│ │ ├── TagRequest.php
│ │ ├── ScrapeRequest.php
│ │ ├── UtensilRequest.php
│ │ ├── CalculatorRequest.php
│ │ ├── IngredientCategoryRequest.php
│ │ ├── PATRequest.php
│ │ ├── CocktailMethodRequest.php
│ │ ├── CocktailScrapeRequest.php
│ │ ├── ShelfIngredientsRequest.php
│ │ ├── GlassRequest.php
│ │ ├── CollectionRequest.php
│ │ ├── NoteRequest.php
│ │ ├── ImageRequest.php
│ │ ├── ImageUpdateRequest.php
│ │ ├── RegisterRequest.php
│ │ ├── PriceCategoryRequest.php
│ │ ├── IngredientsBatchRequest.php
│ │ ├── ExportRequest.php
│ │ ├── RatingRequest.php
│ │ ├── ImportRequest.php
│ │ ├── UpdateUserRequest.php
│ │ └── ImportFileRequest.php
│ ├── Filters
│ │ ├── CocktailMethodQueryFilter.php
│ │ ├── GlassQueryFilter.php
│ │ ├── NoteQueryFilter.php
│ │ └── CollectionQueryFilter.php
│ └── Resources
│ │ ├── TokenResource.php
│ │ ├── UserBasicResource.php
│ │ └── UserShoppingListResource.php
├── Metrics
│ ├── BaseMetrics.php
│ ├── ApiRequestDuration.php
│ ├── SQLDuration.php
│ ├── TotalActiveUsers.php
│ └── TotalBars.php
├── helpers.php
├── Services
│ ├── Auth
│ │ ├── PocketIdExtendSocialite.php
│ │ ├── RegisterUserService.php
│ │ └── OauthProvider.php
│ ├── Image
│ │ ├── ImageThumbnailService.php
│ │ ├── ImageResizeService.php
│ │ └── ImageHashingService.php
│ └── Feeds
│ │ └── FeedsClient.php
├── Providers
│ ├── BroadcastServiceProvider.php
│ ├── AuthServiceProvider.php
│ └── HorizonServiceProvider.php
├── Scraper
│ ├── SchemaModel.php
│ ├── Sites
│ │ └── MakeMeACocktail.php
│ └── Site.php
├── Policies
│ ├── NotePolicy.php
│ ├── MenuPolicy.php
│ ├── ExportPolicy.php
│ ├── PersonalAccessTokenPolicy.php
│ ├── TagPolicy.php
│ ├── GlassPolicy.php
│ ├── UtensilPolicy.php
│ ├── CocktailMethodPolicy.php
│ ├── CalculatorPolicy.php
│ └── UserPolicy.php
├── BarContext.php
├── Rules
│ ├── ValidCurrency.php
│ └── SubscriberImagesCount.php
├── Listeners
│ ├── ActivateBars.php
│ └── DeactivateBars.php
├── Jobs
│ └── StartBarOptimization.php
├── Console
│ ├── Kernel.php
│ └── Commands
│ │ ├── RebuildHierarchy.php
│ │ ├── BarTestEmail.php
│ │ ├── BarClearMetrics.php
│ │ ├── BarSearchRefresh.php
│ │ └── BarAnon.php
└── ZipUtils.php
├── .gitattributes
├── .editorconfig
├── .gitignore
├── rector.php
├── .env.testing
├── .dockerignore
├── SECURITY.md
├── .env.dev
├── config
└── cors.php
├── .env.dist
├── LICENSE
└── routes
└── web.php
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite*
2 |
--------------------------------------------------------------------------------
/.spectral.yml:
--------------------------------------------------------------------------------
1 | extends: ["spectral:oas"]
--------------------------------------------------------------------------------
/bootstrap/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/tests/fixtures/sus_image.txt:
--------------------------------------------------------------------------------
1 | this is an image
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/storage/app/public/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/debugbar/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/http_cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/app/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !public/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/storage/framework/testing/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/views/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite*
2 | *.zip
3 | bar-assistant/
4 |
--------------------------------------------------------------------------------
/storage/framework/cache/data/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/sessions/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !data/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/resources/art/art1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlomikus/bar-assistant/HEAD/resources/art/art1.png
--------------------------------------------------------------------------------
/tests/fixtures/test.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlomikus/bar-assistant/HEAD/tests/fixtures/test.jpg
--------------------------------------------------------------------------------
/resources/art/readme-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlomikus/bar-assistant/HEAD/resources/art/readme-logo.png
--------------------------------------------------------------------------------
/tests/fixtures/cocktail.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlomikus/bar-assistant/HEAD/tests/fixtures/cocktail.jpg
--------------------------------------------------------------------------------
/resources/art/readme-header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlomikus/bar-assistant/HEAD/resources/art/readme-header.png
--------------------------------------------------------------------------------
/public/vendor/horizon/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlomikus/bar-assistant/HEAD/public/vendor/horizon/favicon.png
--------------------------------------------------------------------------------
/public/vendor/horizon/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlomikus/bar-assistant/HEAD/public/vendor/horizon/img/favicon.png
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - ./vendor/larastan/larastan/extension.neon
3 | parameters:
4 | paths:
5 | - app/
6 | level: 7
--------------------------------------------------------------------------------
/resources/docker/dist/php.ini:
--------------------------------------------------------------------------------
1 | expose_php = Off
2 |
3 | ; Following values are required by libvips
4 | ffi.enable=true
5 | zend.max_allowed_stack_size=-1
--------------------------------------------------------------------------------
/tests/fixtures/datapack/_meta.json:
--------------------------------------------------------------------------------
1 | {"version":"testing","date":"2024-09-12T13:32:47+00:00","called_from":"Kami\\Cocktail\\External\\Export\\ToDataPack"}
--------------------------------------------------------------------------------
/tests/fixtures/datapack/base_utensils.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "utensil 1",
4 | "description": "utensil description"
5 | }
6 | ]
--------------------------------------------------------------------------------
/resources/views/emails/test.blade.php:
--------------------------------------------------------------------------------
1 |
2 | # Test email
3 |
4 | If you got this email, it means that your email settings are valid.
5 |
6 |
--------------------------------------------------------------------------------
/tests/fixtures/datapack/cocktails/test-cocktail/c-1-img.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlomikus/bar-assistant/HEAD/tests/fixtures/datapack/cocktails/test-cocktail/c-1-img.jpg
--------------------------------------------------------------------------------
/tests/fixtures/datapack/ingredients/test-ingredient/i-1-img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlomikus/bar-assistant/HEAD/tests/fixtures/datapack/ingredients/test-ingredient/i-1-img.png
--------------------------------------------------------------------------------
/storage/framework/.gitignore:
--------------------------------------------------------------------------------
1 | compiled.php
2 | config.php
3 | down
4 | events.scanned.php
5 | maintenance.php
6 | routes.php
7 | routes.scanned.php
8 | schedule-*
9 | services.json
10 |
--------------------------------------------------------------------------------
/tests/fixtures/ingredients.csv:
--------------------------------------------------------------------------------
1 | name,strength,description,origin,sugar_g_per_ml,acidity,distillery
2 | Campari,40,Bitter liquer,Italy,,,
3 | gin,23.3,,,,,
4 | Whiskey,,,,,,
5 | empty,,,,,,
6 |
--------------------------------------------------------------------------------
/resources/docker/localdev/prometheus.yml:
--------------------------------------------------------------------------------
1 | scrape_configs:
2 | - job_name: bar-assistant
3 | scrape_interval: 30s
4 | metrics_path: /metrics
5 | static_configs:
6 | - targets: ['webserver']
--------------------------------------------------------------------------------
/pint.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "psr12",
3 | "rules": {
4 | "no_unused_imports": true,
5 | "ordered_imports": {
6 | "sort_algorithm": "length"
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/app/External/SupportsXML.php:
--------------------------------------------------------------------------------
1 |
2 | # Account deleted
3 |
4 | Your account has been deleted. If you had active subscription, it is now canceled.
5 |
6 | Thanks for using Bar Assistant.
7 |
8 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
3 | *.blade.php diff=html
4 | *.css diff=css
5 | *.html diff=html
6 | *.md diff=markdown
7 | *.php diff=php
8 |
9 | /.github export-ignore
10 | CHANGELOG.md export-ignore
11 | .styleci.yml export-ignore
12 |
--------------------------------------------------------------------------------
/app/Exceptions/IngredientPathTooDeepException.php:
--------------------------------------------------------------------------------
1 | user()->name }} | Shopping list for "{{ bar()->name }}"
2 |
3 | @foreach($shoppingListIngredients as $sli)
4 | - [] {{ $sli->ingredient->name }} x{{ $sli->quantity }}
5 | @endforeach
6 |
--------------------------------------------------------------------------------
/resources/docker/dist/nginx.conf:
--------------------------------------------------------------------------------
1 | access_log off;
2 |
3 | location ~ ^/uploads/ {
4 | add_header 'Access-Control-Allow-Origin' '*';
5 | add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
6 | add_header 'Access-Control-Allow-Headers' '*';
7 | }
--------------------------------------------------------------------------------
/resources/views/emails/password-changed.blade.php:
--------------------------------------------------------------------------------
1 |
2 | # Password changed
3 |
4 | The password for your account was changed succesfully.
5 |
6 | If you did not change your password, please contact us as soon as possible.
7 |
8 |
--------------------------------------------------------------------------------
/app/Models/Enums/UserRoleEnum.php:
--------------------------------------------------------------------------------
1 | $sourceArray
11 | */
12 | public static function fromCSV(array $sourceArray): self;
13 | }
14 |
--------------------------------------------------------------------------------
/tests/fixtures/datapack/base_price_categories.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "price 1",
4 | "currency": "NAM",
5 | "description": "Price description"
6 | },
7 | {
8 | "name": "price 2",
9 | "currency": "TKL",
10 | "description": null
11 | }
12 | ]
--------------------------------------------------------------------------------
/app/Models/CocktailPrice.php:
--------------------------------------------------------------------------------
1 | */
10 | public array $inputs = [];
11 |
12 | /** @var array */
13 | public array $results = [];
14 | }
15 |
--------------------------------------------------------------------------------
/resources/views/emails/confirm-account.blade.php:
--------------------------------------------------------------------------------
1 |
2 | # Welcome to Bar Assistant
3 |
4 | Please confirm your email address by clicking the link below.
5 |
6 |
7 | Confirm email address
8 |
9 |
10 | If you did not create an account, no further action is required.
11 |
12 |
--------------------------------------------------------------------------------
/app/External/ForceUnitConvertEnum.php:
--------------------------------------------------------------------------------
1 |
2 | # Reset your password
3 |
4 | You are receiving this email because we received a password reset request for your account.
5 |
6 | If you did not request a password reset, no further action is required.
7 |
8 |
9 | Click here to reset your password
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/html/footer.blade.php:
--------------------------------------------------------------------------------
1 |
2 | |
3 |
10 | |
11 |
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /public/build
3 | /public/hot
4 | /public/storage
5 | /public/uploads
6 | /storage/*.key
7 | /vendor
8 | .zed
9 | .env
10 | .env.backup
11 | .phpunit.result.cache
12 | Homestead.json
13 | Homestead.yaml
14 | auth.json
15 | npm-debug.log
16 | yarn-error.log
17 | /.idea
18 | /.vscode
19 | notes.sql
20 | composer.phar
21 | .phpunit.cache
22 | /resources/data
23 | litestream.yml
24 | .todo
25 |
--------------------------------------------------------------------------------
/app/External/SupportsDraft2.php:
--------------------------------------------------------------------------------
1 | $sourceArray
11 | */
12 | public static function fromDraft2Array(array $sourceArray): self;
13 |
14 | /**
15 | * @return array
16 | */
17 | public function toDraft2Array(): array;
18 | }
19 |
--------------------------------------------------------------------------------
/app/OpenAPI/Schemas/APIError.php:
--------------------------------------------------------------------------------
1 | $sourceArray
11 | */
12 | public static function fromDataPackArray(array $sourceArray): self;
13 |
14 | /**
15 | * @return array
16 | */
17 | public function toDataPackArray(): array;
18 | }
19 |
--------------------------------------------------------------------------------
/app/Http/Middleware/EncryptCookies.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [
15 | //
16 | ];
17 | }
18 |
--------------------------------------------------------------------------------
/app/Metrics/BaseMetrics.php:
--------------------------------------------------------------------------------
1 | withPaths([
9 | __DIR__ . '/app',
10 | __DIR__ . '/bootstrap',
11 | __DIR__ . '/config',
12 | __DIR__ . '/public',
13 | __DIR__ . '/resources',
14 | __DIR__ . '/routes',
15 | __DIR__ . '/tests',
16 | ])
17 | ->withPhpSets();
18 |
--------------------------------------------------------------------------------
/app/Http/Middleware/VerifyCsrfToken.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [
15 | //
16 | ];
17 | }
18 |
--------------------------------------------------------------------------------
/app/helpers.php:
--------------------------------------------------------------------------------
1 | make(BarContext::class)->getBar();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/Http/Middleware/Authenticate.php:
--------------------------------------------------------------------------------
1 | expectsJson()) {
13 | return '/';
14 | }
15 |
16 | return null;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/OpenAPI/Schemas/CocktailMethodRequest.php:
--------------------------------------------------------------------------------
1 | extendSocialite('pocketid', Provider::class);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/html/header.blade.php:
--------------------------------------------------------------------------------
1 | @props(['url'])
2 |
3 |
13 |
14 |
--------------------------------------------------------------------------------
/tests/fixtures/external/markdown.md:
--------------------------------------------------------------------------------
1 | # Gin and Tonic
2 | [Recipe source](https://barassistant.app)
3 | Cocktail description goes here
4 |
5 | 
6 |
7 | ## Ingredients
8 | - 45 ml - 60 ml Gin - Ingredient note
9 |
10 | ## Instructions
11 | Cocktail instructions go here
12 |
13 | ### Garnish
14 | Straw and lemon wheel
15 |
16 | ---
17 | - ABV: 32.3
18 | - Glass: Highball
19 | - Method: Shake
20 |
--------------------------------------------------------------------------------
/.env.testing:
--------------------------------------------------------------------------------
1 | APP_NAME="Bar Assistant"
2 | APP_ENV=production
3 | APP_KEY=
4 | APP_DEBUG=false
5 | APP_URL=http://localhost
6 |
7 | DB_CONNECTION=sqlite
8 | DB_FOREIGN_KEYS=true
9 | DB_DATABASE=":memory:"
10 |
11 | SCOUT_DRIVER=meilisearch
12 | MEILISEARCH_HOST=http://meilisearch:7700
13 | MEILISEARCH_KEY=masterKeyThatIsReallyReallyLong4Real
14 |
15 | SPEC_PATH=./docs
16 |
17 | BILLING_PRODUCT_PRICES="pri_12345|pri_67890"
18 | METRICS_ENABLED=true
19 | METRICS_ALLOWED_IPS=*
20 |
--------------------------------------------------------------------------------
/public/vendor/horizon/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/app.js": "/app.js?id=4999da9248177ed487693daec2a7d3fe",
3 | "/app-dark.css": "/app-dark.css?id=dcaca44a9f0f1d019e3cd3d76c3cb8fd",
4 | "/app.css": "/app.css?id=14e3bcd1f1b1cf88e63e945529c4d0ce",
5 | "/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f",
6 | "/img/horizon.svg": "/img/horizon.svg?id=904d5b5185fefb09035384e15bfca765",
7 | "/img/sprite.svg": "/img/sprite.svg?id=afc4952b74895bdef3ab4ebe9adb746f"
8 | }
9 |
--------------------------------------------------------------------------------
/app/Http/Middleware/PreventRequestsDuringMaintenance.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [
15 | //
16 | ];
17 | }
18 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrustHosts.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | public function hosts()
15 | {
16 | return [
17 | $this->allSubdomainsOfApplicationUrl(),
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/resources/views/emails/subscription-changed.blade.php:
--------------------------------------------------------------------------------
1 |
2 | # Subscription updated
3 |
4 | You have updated your subscription.
5 |
6 | @if ($changeType === 'resume')
7 | Your subscription is now **resumed**.
8 | @endif
9 |
10 | @if ($changeType === 'pause')
11 | Your subscription is now **paused**. You can resume it at any time from your profile.
12 | @endif
13 |
14 | @if ($changeType === 'cancel')
15 | Your subscription is now **canceled**.
16 | @endif
17 |
18 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrimStrings.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [
15 | 'current_password',
16 | 'password',
17 | 'password_confirmation',
18 | ];
19 | }
20 |
--------------------------------------------------------------------------------
/tests/CreatesApplication.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class)->bootstrap();
19 |
20 | return $app;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Services/Image/ImageThumbnailService.php:
--------------------------------------------------------------------------------
1 | thumbnail_image($size)->writeToBuffer('.webp', ['Q' => $quality]);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/OpenAPI/Schemas/NoteRequest.php:
--------------------------------------------------------------------------------
1 | > */
12 | public array $instructions = [];
13 | /** @var array */
14 | public array $tags = [];
15 | /** @var array */
16 | public array $ingredients = [];
17 | public ?string $image = null;
18 | public ?string $author = null;
19 | }
20 |
--------------------------------------------------------------------------------
/app/OpenAPI/Schemas/ExportRequest.php:
--------------------------------------------------------------------------------
1 | */
13 | #[OAT\Property(type: 'array', items: new OAT\Items(type: 'object', properties: [
14 | new OAT\Property(property: 'id', type: 'integer'),
15 | new OAT\Property(property: 'quantity', type: 'integer'),
16 | ]))]
17 | public array $ingredients;
18 | }
19 |
--------------------------------------------------------------------------------
/database/factories/NoteFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class NoteFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'note' => fake()->paragraph(),
21 | ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Policies/NotePolicy.php:
--------------------------------------------------------------------------------
1 | id === $note->user_id;
18 | }
19 |
20 | public function delete(User $user, Note $note): bool
21 | {
22 | return $user->id === $note->user_id;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/Services/Image/ImageResizeService.php:
--------------------------------------------------------------------------------
1 | height;
16 |
17 | if ($image->height > $height) {
18 | $image = $image->resize($scale);
19 | }
20 |
21 | return $image;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/OpenAPI/Parameters/PageParameter.php:
--------------------------------------------------------------------------------
1 | */
15 | #[OAT\Property(type: 'object', additionalProperties: new OAT\AdditionalProperties(type: 'array', items: new OAT\Items(type: 'string')))]
16 | public array $errors;
17 | }
18 |
--------------------------------------------------------------------------------
/app/Http/Middleware/ValidateSignature.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [
15 | // 'fbclid',
16 | // 'utm_campaign',
17 | // 'utm_content',
18 | // 'utm_medium',
19 | // 'utm_source',
20 | // 'utm_term',
21 | ];
22 | }
23 |
--------------------------------------------------------------------------------
/app/Policies/MenuPolicy.php:
--------------------------------------------------------------------------------
1 | isBarAdmin(bar()->id) || $user->isBarModerator(bar()->id);
17 | }
18 |
19 | public function update(User $user): bool
20 | {
21 | return $user->isBarAdmin(bar()->id) || $user->isBarModerator(bar()->id);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/OpenAPI/WrapObjectWithData.php:
--------------------------------------------------------------------------------
1 | created_user_id === $user->id;
18 | }
19 |
20 | public function download(User $user, Export $token): bool
21 | {
22 | return $token->created_user_id === $user->id;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/Models/Note.php:
--------------------------------------------------------------------------------
1 | */
14 | use HasFactory;
15 |
16 | /**
17 | * @return MorphTo
18 | */
19 | public function noteable(): MorphTo
20 | {
21 | return $this->morphTo();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/OpenAPI/Parameters/DatabaseIdParameter.php:
--------------------------------------------------------------------------------
1 | */
14 | use HasFactory;
15 |
16 | /**
17 | * @return MorphTo
18 | */
19 | public function rateable(): MorphTo
20 | {
21 | return $this->morphTo();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Providers/AuthServiceProvider.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $policies = [
15 | ];
16 |
17 | /**
18 | * Register any authentication / authorization services.
19 | *
20 | * @return void
21 | */
22 | public function boot()
23 | {
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/database/factories/TagFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class TagFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'name' => fake()->name(),
21 | 'bar_id' => \Kami\Cocktail\Models\Bar::factory(),
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/resources/views/emails/master.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Bar Assistant
13 | Your personal bar assistant
14 |
15 | @yield('content')
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/OpenAPI/Parameters/BarIdHeaderParameter.php:
--------------------------------------------------------------------------------
1 | currentBar;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/Policies/PersonalAccessTokenPolicy.php:
--------------------------------------------------------------------------------
1 | hasActiveSubscription();
18 | }
19 |
20 | public function delete(User $user, PersonalAccessToken $token): bool
21 | {
22 | return $user->tokens->contains('id', $token->id);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | 5.x | :white_check_mark: |
8 | | 4.x | :white_check_mark: |
9 | | < 3.x | :x: |
10 |
11 | ## Reporting a Vulnerability
12 |
13 | If you have discovered a potential security vulnerability in any part of this project, please report it immediately by sending an email to contact@karlomikus.com with details about the vulnerability and steps to reproduce if possible.
14 |
15 | You should receive a response within 48 hours. If for some reason you do not, please follow up within two weeks.
16 |
--------------------------------------------------------------------------------
/app/Models/PersonalAccessToken.php:
--------------------------------------------------------------------------------
1 | $abilities
13 | * @property string $last_used_at
14 | * @property string $created_at
15 | * @property string $expires_at
16 | */
17 | class PersonalAccessToken extends SanctumToken
18 | {
19 | /** @use \Illuminate\Database\Eloquent\Factories\HasFactory<\Database\Factories\PersonalAccessTokenFactory> */
20 | use HasFactory;
21 | }
22 |
--------------------------------------------------------------------------------
/database/factories/RatingFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class RatingFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'rating' => fake()->numberBetween(1, 5),
21 | 'user_id' => \Kami\Cocktail\Models\User::factory(),
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/OpenAPI/Schemas/ProfileSettings.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | public function toArray(): array
21 | {
22 | return [
23 | 'language' => $this->language,
24 | 'theme' => $this->theme,
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Models/ValueObjects/CocktailIngredientFormatter.php:
--------------------------------------------------------------------------------
1 | name;
19 | $optional = $this->optional === true ? ' (optional)' : '';
20 |
21 | return trim(sprintf('%s %s%s', (string) $this->amount, $name, $optional));
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Models/Concerns/HasAuthors.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | public function createdUser(): BelongsTo
16 | {
17 | return $this->belongsTo(User::class, 'created_user_id');
18 | }
19 |
20 | /**
21 | * @return BelongsTo
22 | */
23 | public function updatedUser(): BelongsTo
24 | {
25 | return $this->belongsTo(User::class, 'updated_user_id');
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Models/UserShoppingList.php:
--------------------------------------------------------------------------------
1 | */
14 | use HasFactory;
15 |
16 | /**
17 | * @return BelongsTo
18 | */
19 | public function ingredient(): BelongsTo
20 | {
21 | return $this->belongsTo(Ingredient::class);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/database/factories/BarIngredientFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class BarIngredientFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'ingredient_id' => \Kami\Cocktail\Models\Ingredient::factory(),
21 | 'bar_id' => \Kami\Cocktail\Models\Bar::factory(),
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/database/factories/GlassFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class GlassFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'name' => fake()->name(),
21 | 'description' => fake()->paragraph(),
22 | 'bar_id' => \Kami\Cocktail\Models\Bar::factory(),
23 | ];
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/database/factories/UtensilFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class UtensilFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'name' => fake()->name(),
21 | 'description' => fake()->paragraph(),
22 | 'bar_id' => \Kami\Cocktail\Models\Bar::factory(),
23 | ];
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/resources/views/emails/subscribed.blade.php:
--------------------------------------------------------------------------------
1 |
2 | # You upgraded to "Mixologist" plan, cheers!
3 |
4 | You now have access to all of the following features:
5 |
6 | - Create and manage up to 10 bars
7 | - Add unlimited cocktail recipes
8 | - Invite users to your bar
9 | - Share your recipes with bar members
10 | - Manage user roles
11 | - Enable sharing recipes via public links
12 | - Add more images to recipes
13 | - Add unlimited cocktail collections
14 | - No rate limits for import actions
15 | - Create personal access API tokens
16 |
17 | These features are automatically activated and you don't have to do anything.
18 |
19 | Enjoy,
20 | Karlo
21 |
22 |
--------------------------------------------------------------------------------
/app/OpenAPI/Schemas/FileDownloadLink.php:
--------------------------------------------------------------------------------
1 | crawler->filterXPath('//div[contains(text(), "Details")]/following::div[2]/div/div')->each(function ($node) use (&$result) {
21 | $result[] = trim((string) $node->text());
22 | });
23 |
24 | return $result;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 |
2 |
3 | Options -MultiViews -Indexes
4 |
5 |
6 | RewriteEngine On
7 |
8 | # Handle Authorization Header
9 | RewriteCond %{HTTP:Authorization} .
10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
11 |
12 | # Redirect Trailing Slashes If Not A Folder...
13 | RewriteCond %{REQUEST_FILENAME} !-d
14 | RewriteCond %{REQUEST_URI} (.+)/$
15 | RewriteRule ^ %1 [L,R=301]
16 |
17 | # Send Requests To Front Controller...
18 | RewriteCond %{REQUEST_FILENAME} !-d
19 | RewriteCond %{REQUEST_FILENAME} !-f
20 | RewriteRule ^ index.php [L]
21 |
22 |
--------------------------------------------------------------------------------
/database/factories/CalculatorFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class CalculatorFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'name' => fake()->name(),
21 | 'description' => fake()->optional()->paragraph(),
22 | 'bar_id' => \Kami\Cocktail\Models\Bar::factory(),
23 | ];
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Controller.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | public function toArray(): array
21 | {
22 | return [
23 | 'default_units' => $this->defaultUnits,
24 | 'default_currency' => $this->defaultCurrency,
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/database/factories/UserIngredientFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class UserIngredientFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'ingredient_id' => \Kami\Cocktail\Models\Ingredient::factory(),
21 | 'bar_membership_id' => \Kami\Cocktail\Models\BarMembership::factory(),
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/fixtures/datapack/ingredients/test-ingredient/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "_id": "test-ingredient",
3 | "_parent_id": null,
4 | "name": "Test ingredient",
5 | "strength": 37.75,
6 | "description": "Description of ingredient",
7 | "origin": "French Guiana",
8 | "color": "#b474de",
9 | "category": "category 1",
10 | "created_at": "1976-01-23T22:25:11+00:00",
11 | "updated_at": "1998-01-08T13:41:44+00:00",
12 | "images": [
13 | {
14 | "uri": "file:///i-1-img.png",
15 | "sort": 1,
16 | "placeholder_hash": null,
17 | "copyright": "Image copyright"
18 | }
19 | ],
20 | "ingredient_parts": [],
21 | "prices": []
22 | }
--------------------------------------------------------------------------------
/app/Http/Requests/TagRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'name' => 'required',
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Rules/ValidCurrency.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class CocktailFavoriteFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'cocktail_id' => \Kami\Cocktail\Models\Cocktail::factory(),
21 | 'bar_membership_id' => \Kami\Cocktail\Models\BarMembership::factory(),
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/database/factories/CollectionFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class CollectionFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'name' => fake()->name(),
21 | 'description' => fake()->paragraph(),
22 | 'bar_membership_id' => \Kami\Cocktail\Models\BarMembership::factory(),
23 | ];
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.env.dev:
--------------------------------------------------------------------------------
1 | APP_NAME="Bar Assistant"
2 | APP_ENV=local
3 | APP_KEY=
4 | APP_DEBUG=true
5 | APP_URL=http://localhost:8000
6 |
7 | LOG_CHANNEL=stack
8 | LOG_DEPRECATIONS_CHANNEL=null
9 | LOG_LEVEL=debug
10 |
11 | DB_CONNECTION=sqlite
12 | DB_FOREIGN_KEYS=true
13 |
14 | BROADCAST_DRIVER=log
15 | CACHE_DRIVER=redis
16 | FILESYSTEM_DISK=local
17 | QUEUE_CONNECTION=sync
18 | SESSION_DRIVER=redis
19 | SESSION_LIFETIME=120
20 | RESPONSE_CACHE_ENABLED=false
21 |
22 | MEMCACHED_HOST=127.0.0.1
23 |
24 | REDIS_HOST=redis
25 | REDIS_PASSWORD=null
26 | REDIS_PORT=6379
27 |
28 | SCOUT_DRIVER=meilisearch
29 | MEILISEARCH_HOST=http://host.docker.internal:7700
30 | MEILISEARCH_KEY=masterKeyThatIsReallyReallyLong4Real
31 |
32 | SPEC_PATH=./docs
33 |
--------------------------------------------------------------------------------
/app/Metrics/ApiRequestDuration.php:
--------------------------------------------------------------------------------
1 | registry->getOrRegisterHistogram(
12 | $this->getDefaultNamespace(),
13 | 'api_request_processing_milliseconds',
14 | 'Time spent processing API request',
15 | ['route', 'method', 'status'],
16 | [100, 250, 500, 1000, 2500, 5000, 10000]
17 | );
18 |
19 | $metric->observe($time * 1000, ['/' . $route, $method, (string) $status]);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/database/factories/ComplexIngredientFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class ComplexIngredientFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'main_ingredient_id' => \Kami\Cocktail\Models\Ingredient::factory(),
21 | 'ingredient_id' => \Kami\Cocktail\Models\Ingredient::factory(),
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/database/factories/IngredientCategoryFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class IngredientCategoryFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'name' => fake()->name(),
21 | 'description' => fake()->paragraph(),
22 | 'bar_id' => \Kami\Cocktail\Models\Bar::factory(),
23 | ];
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Http/Requests/ScrapeRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'source' => 'required|url',
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Http/Requests/UtensilRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'name' => 'required',
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/database/factories/CocktailMethodFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class CocktailMethodFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'name' => fake()->name(),
21 | 'dilution_percentage' => fake()->numberBetween(0, 50),
22 | 'bar_id' => \Kami\Cocktail\Models\Bar::factory(),
23 | ];
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/resources/art/logo.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/app/Http/Requests/CalculatorRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'name' => 'required',
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Models/Concerns/HasBarAwareScope.php:
--------------------------------------------------------------------------------
1 | $query
15 | * @return Builder<\Illuminate\Database\Eloquent\Model>
16 | */
17 | public function scopeFilterByBar(Builder $query, ?string $alias = null): Builder
18 | {
19 | $col = 'bar_id';
20 | if ($alias !== null) {
21 | $col = $alias . '.' . $col;
22 | }
23 |
24 | return $query->where($col, bar()->id);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/database/factories/BarFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class BarFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'name' => fake()->name(),
21 | 'description' => fake()->paragraph(),
22 | 'search_token' => 'test-token',
23 | 'created_user_id' => \Kami\Cocktail\Models\User::factory(),
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Requests/IngredientCategoryRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'name' => 'required',
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/database/factories/MenuFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class MenuFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'bar_id' => \Kami\Cocktail\Models\Bar::factory(),
21 | 'is_enabled' => fake()->boolean(),
22 | 'created_at' => fake()->dateTime(),
23 | 'updated_at' => fake()->optional()->dateTime(),
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Scraper/Site.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | public function tags(): array;
21 |
22 | public function glass(): ?string;
23 |
24 | /**
25 | * @return array
26 | */
27 | public function ingredients(): array;
28 |
29 | public function garnish(): ?string;
30 |
31 | /**
32 | * @return null|array
33 | */
34 | public function image(): ?array;
35 | }
36 |
--------------------------------------------------------------------------------
/database/factories/ImageFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class ImageFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'copyright' => fake()->paragraph(),
21 | 'file_path' => fake()->filePath(),
22 | 'file_extension' => fake()->fileExtension(),
23 | 'created_user_id' => \Kami\Cocktail\Models\User::factory(),
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Metrics/SQLDuration.php:
--------------------------------------------------------------------------------
1 | registry->getOrRegisterHistogram(
15 | $this->getDefaultNamespace(),
16 | 'sql_duration_milliseconds',
17 | 'SQL duration',
18 | ['route'],
19 | [100, 250, 500, 1000, 2500]
20 | );
21 |
22 | DB::listen(function (QueryExecuted $query) use ($metric, $route) {
23 | $metric->observe($query->time, [$route]);
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/database/factories/PriceCategoryFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class PriceCategoryFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'name' => fake()->sentence(3),
21 | 'description' => fake()->paragraph(),
22 | 'currency' => fake()->currencyCode(),
23 | 'bar_id' => \Kami\Cocktail\Models\Bar::factory(),
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/database/migrations/2024_05_11_152732_add_settings_col_to_bars.php:
--------------------------------------------------------------------------------
1 | json('settings')->nullable();
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('bars', function (Blueprint $table) {
24 | $table->dropColumn('settings');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/app/Listeners/ActivateBars.php:
--------------------------------------------------------------------------------
1 | subscription;
16 |
17 | /** @var \Kami\Cocktail\Models\User */
18 | $user = $subscription->billable;
19 |
20 | Log::info('User "' . $user->email . '" created subscription.');
21 |
22 | foreach ($user->ownedBars as $bar) {
23 | Cache::forget('ba:bar:' . $bar->id);
24 | }
25 |
26 | $user->activateBars();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_05_152429_add_settings_column_to_users.php:
--------------------------------------------------------------------------------
1 | json('settings')->nullable();
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('users', function (Blueprint $table) {
24 | $table->dropColumn('settings');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/app/Http/Requests/PATRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'expires_at' => 'date|after:today',
30 | 'abilities' => 'array|min:1',
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/OpenAPI/RateLimitResponse.php:
--------------------------------------------------------------------------------
1 | integer('year')->nullable();
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('cocktails', function (Blueprint $table) {
24 | $table->dropColumn('year');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/app/Listeners/DeactivateBars.php:
--------------------------------------------------------------------------------
1 | subscription;
16 |
17 | /** @var \Kami\Cocktail\Models\User */
18 | $user = $subscription->billable;
19 |
20 | Log::info('User "' . $user->email . '" canceled subscription.');
21 |
22 | foreach ($user->ownedBars as $bar) {
23 | Cache::forget('ba:bar:' . $bar->id);
24 | }
25 |
26 | $user->deactivateBars();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/OpenAPI/Schemas/ImageRequest.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class UserShoppingListFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'ingredient_id' => \Kami\Cocktail\Models\Ingredient::factory(),
21 | 'bar_membership_id' => \Kami\Cocktail\Models\BarMembership::factory(),
22 | 'quantity' => fake()->numberBetween(1, 10),
23 | ];
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/database/migrations/2024_08_11_181521_add_meilisearch_token_to_bars.php:
--------------------------------------------------------------------------------
1 | string('search_token')->nullable();
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('bars', function (Blueprint $table) {
24 | $table->dropColumn('search_token');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/database/migrations/2024_08_17_120824_add_timestamps_to_price_categories.php:
--------------------------------------------------------------------------------
1 | timestamps();
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('price_categories', function (Blueprint $table) {
24 | $table->dropTimestamps();
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/database/migrations/2025_07_25_201245_add_is_public_column_to_bars.php:
--------------------------------------------------------------------------------
1 | boolean('is_public')->default(false);
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('bars', function (Blueprint $table) {
24 | $table->dropColumn('is_public');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/resources/views/elements.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Bar Assistant · API Documentation
7 |
8 |
9 |
10 |
11 |
12 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_28_182053_add_default_units_to_ingredients.php:
--------------------------------------------------------------------------------
1 | string('units')->nullable();
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('ingredients', function (Blueprint $table) {
24 | $table->dropColumn('units');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/app/Http/Filters/CocktailMethodQueryFilter.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | final class CocktailMethodQueryFilter extends QueryBuilder
15 | {
16 | public function __construct()
17 | {
18 | parent::__construct(CocktailMethod::query());
19 |
20 | $this
21 | ->allowedFilters([
22 | AllowedFilter::partial('name'),
23 | ])
24 | ->defaultSort('name')
25 | ->withCount('cocktails')
26 | ->filterByBar();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/OpenAPI/Schemas/CalculatorBlockSettings.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | public function toArray(): array
23 | {
24 | return [
25 | 'suffix' => $this->suffix,
26 | 'prefix' => $this->prefix,
27 | 'decimal_places' => $this->decimalPlaces,
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/OpenAPI/Schemas/CalculatorSolveRequest.php:
--------------------------------------------------------------------------------
1 | $inputs
14 | */
15 | public function __construct(
16 | #[OAT\Property(type: 'object', additionalProperties: new OAT\AdditionalProperties(type: 'string'))]
17 | public array $inputs,
18 | ) {
19 | }
20 |
21 | /**
22 | * @param array $source
23 | */
24 | public static function fromArray(array $source): self
25 | {
26 | return new self(
27 | $source['inputs'],
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_02_201313_add_images_index.php:
--------------------------------------------------------------------------------
1 | index('imageable_id', 'images_imageable_id_idx');
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('images', function (Blueprint $table) {
24 | $table->dropIndex('images_imageable_id_idx');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/app/Http/Requests/CocktailMethodRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'name' => 'required',
30 | 'dilution_percentage' => 'required|integer',
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Requests/CocktailScrapeRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'url' => 'sometimes|required|url',
30 | 'json' => 'sometimes|required',
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Requests/ShelfIngredientsRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'ingredients' => 'required|array',
30 | 'ingredients.*' => 'integer',
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/External/ExportTypeEnum.php:
--------------------------------------------------------------------------------
1 | 'datapack',
23 | self::Schema => 'schema',
24 | self::Markdown => 'markdown',
25 | self::JSONLD => 'json-ld',
26 | self::XML => 'xml',
27 | self::YAML => 'yaml',
28 | };
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrustProxies.php:
--------------------------------------------------------------------------------
1 | |string|null
14 | */
15 | protected $proxies;
16 |
17 | /**
18 | * The headers that should be used to detect proxies.
19 | *
20 | * @var int
21 | */
22 | protected $headers =
23 | Request::HEADER_X_FORWARDED_FOR |
24 | Request::HEADER_X_FORWARDED_HOST |
25 | Request::HEADER_X_FORWARDED_PORT |
26 | Request::HEADER_X_FORWARDED_PROTO |
27 | Request::HEADER_X_FORWARDED_AWS_ELB;
28 | }
29 |
--------------------------------------------------------------------------------
/app/Metrics/TotalActiveUsers.php:
--------------------------------------------------------------------------------
1 | whereNotNull('email_verified_at')->count();
15 | } else {
16 | $total = DB::table('users')->count();
17 | }
18 |
19 | $metric = $this->registry->getOrRegisterGauge(
20 | $this->getDefaultNamespace(),
21 | 'active_users_total',
22 | 'Total number of users with confirmed email'
23 | );
24 |
25 | $metric->set($total);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/database/migrations/2023_10_11_210049_add_share_to_collections_table.php:
--------------------------------------------------------------------------------
1 | boolean('is_bar_shared')->default(false);
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('collections', function (Blueprint $table) {
24 | $table->dropColumn('is_bar_shared');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/database/migrations/2024_08_10_172139_add_quantity_to_shopping_list.php:
--------------------------------------------------------------------------------
1 | integer('quantity')->default(1);
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('user_shopping_lists', function (Blueprint $table) {
24 | $table->dropColumn('quantity');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/app/Http/Requests/GlassRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'name' => 'required',
30 | 'volume' => 'numeric|nullable',
31 | 'images' => 'array|max:1',
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/database/factories/PersonalAccessTokenFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class PersonalAccessTokenFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'token' => fake()->uuid(),
21 | 'name' => fake()->userAgent(),
22 | 'last_used_at' => fake()->dateTime(),
23 | 'created_at' => fake()->dateTime(),
24 | 'expires_at' => fake()->dateTime(),
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Http/Requests/CollectionRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'name' => 'required',
30 | 'cocktails' => 'array',
31 | 'share_in_bar' => 'boolean',
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Http/Requests/NoteRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'note' => 'required',
30 | 'resource_id' => 'required|integer',
31 | 'resource' => 'required',
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Jobs/StartBarOptimization.php:
--------------------------------------------------------------------------------
1 | optimize($this->barId);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/fixtures/datapack/cocktails/test-cocktail/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "_id": "test-cocktail",
3 | "name": "Test cocktail",
4 | "instructions": "Cocktail instructions",
5 | "created_at": "1979-12-23T09:07:48+00:00",
6 | "updated_at": "1983-01-24T11:37:19+00:00",
7 | "description": "Cocktail description",
8 | "source": "http://www.bins.org/fugiat-reprehenderit-necessitatibus-sapiente-quia",
9 | "garnish": "Lemon wheel",
10 | "abv": 37.77,
11 | "tags": [],
12 | "glass": "glass 2",
13 | "method": "method 1",
14 | "utensils": [],
15 | "images": [
16 | {
17 | "uri": "file:///c-1-img.jpg",
18 | "sort": 1,
19 | "placeholder_hash": null,
20 | "copyright": "Random image"
21 | }
22 | ],
23 | "ingredients": []
24 | }
--------------------------------------------------------------------------------
/app/Http/Requests/ImageRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'images' => 'array',
30 | 'images.*.id' => 'sometimes|integer',
31 | 'images.*.sort' => 'integer',
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/database/factories/BarMembershipFactory.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class BarMembershipFactory extends Factory
12 | {
13 | /**
14 | * Define the model's default state.
15 | *
16 | * @return array
17 | */
18 | public function definition()
19 | {
20 | return [
21 | 'is_active' => true,
22 | 'user_role_id' => UserRoleEnum::Admin->value,
23 | 'user_id' => \Kami\Cocktail\Models\User::factory(),
24 | 'bar_id' => \Kami\Cocktail\Models\Bar::factory(),
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/database/migrations/2024_01_01_145353_add_parent_sub_column.php:
--------------------------------------------------------------------------------
1 | boolean('use_parent_as_substitute')->default(false);
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('bar_memberships', function (Blueprint $table) {
24 | $table->dropColumn('use_parent_as_substitute');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_17_145628_add_cocktail_tag_index.php:
--------------------------------------------------------------------------------
1 | index('cocktail_id', 'cocktail_tag_cocktail_id_idx');
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('cocktail_tag', function (Blueprint $table) {
24 | $table->dropIndex('cocktail_tag_cocktail_id_idx');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/tests/Feature/Http/ServerControllerTest.php:
--------------------------------------------------------------------------------
1 | getJson('/api/server/version');
12 |
13 | $response->assertStatus(200);
14 |
15 | $this->assertNotNull($response['data']['type']);
16 | $this->assertNotNull($response['data']['version']);
17 | $this->assertNotNull($response['data']['search_host']);
18 | $this->assertNotNull($response['data']['search_version']);
19 | }
20 |
21 | public function test_openapi_response(): void
22 | {
23 | $response = $this->getJson('/api/server/openapi');
24 |
25 | $response->assertStatus(200);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Http/Filters/GlassQueryFilter.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | final class GlassQueryFilter extends QueryBuilder
15 | {
16 | public function __construct()
17 | {
18 | parent::__construct(Glass::query());
19 |
20 | $this
21 | ->allowedFilters([
22 | AllowedFilter::partial('name'),
23 | ])
24 | ->defaultSort('name')
25 | ->allowedSorts('name', 'created_at')
26 | ->withCount('cocktails')
27 | ->with('images')
28 | ->filterByBar();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/Http/Requests/ImageUpdateRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'image' => 'sometimes|image',
30 | 'image_url' => 'sometimes|url',
31 | 'sort' => 'sometimes|integer',
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Http/Requests/RegisterRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'email' => 'required|unique:users,email',
30 | 'name' => 'required',
31 | 'password' => 'required|min:5',
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/database/migrations/2023_11_22_180057_mark_all_accounts_verified.php:
--------------------------------------------------------------------------------
1 | whereNull('email_verified_at')
18 | ->whereNot('password', 'deleted')
19 | ->update(['email_verified_at' => now()]);
20 | }
21 |
22 | /**
23 | * Reverse the migrations.
24 | */
25 | public function down(): void
26 | {
27 | //
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/database/migrations/2025_05_18_163451_add_complex_ingredients_index.php:
--------------------------------------------------------------------------------
1 | index('main_ingredient_id', 'ci_main_ingredient_idx');
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('complex_ingredients', function (Blueprint $table) {
24 | $table->dropIndex('ci_main_ingredient_idx');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/database/migrations/2025_07_05_080957_add_parent_id_column_to_cocktails.php:
--------------------------------------------------------------------------------
1 | foreignId('parent_cocktail_id')->nullable()->constrained('cocktails')->nullOnDelete();
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('cocktails', function (Blueprint $table) {
24 | $table->dropColumn('parent_cocktail_id');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/app/Http/Requests/PriceCategoryRequest.php:
--------------------------------------------------------------------------------
1 |
26 | */
27 | public function rules()
28 | {
29 | return [
30 | 'name' => 'required',
31 | 'currency' => ['required', 'size:3', new ValidCurrency()],
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Http/Requests/IngredientsBatchRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'ingredients' => 'required|array',
30 | 'ingredients.*.id' => 'required',
31 | 'ingredients.*.quantity' => 'integer',
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/database/migrations/2024_11_09_133632_migrate_bar_shelf_data.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class MenuCocktailFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'category_name' => fake()->name(),
21 | 'sort' => 1,
22 | 'menu_id' => \Kami\Cocktail\Models\Menu::factory(),
23 | 'cocktail_id' => \Kami\Cocktail\Models\Cocktail::factory(),
24 | 'price' => fake()->randomNumber(2),
25 | 'currency' => fake()->currencyCode(),
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Controllers/MetricsController.php:
--------------------------------------------------------------------------------
1 | render($registry->getMetricFamilySamples());
21 | } catch (Throwable $e) {
22 | Log::error('Unable to render metrics: ' . $e->getMessage());
23 | $result = '';
24 | }
25 |
26 | return new Response($result, 200, ['Content-Type' => RenderTextFormat::MIME_TYPE]);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/fixtures/external/json-ld.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://schema.org",
3 | "@type": "Recipe",
4 | "author": {
5 | "@type": "Organization",
6 | "name": "Bar Assistant | Source: https://barassistant.app"
7 | },
8 | "name": "Gin and Tonic",
9 | "datePublished": "2020-01-01T12:00:00+00:00",
10 | "description": "Cocktail description goes here",
11 | "recipeInstructions": "Cocktail instructions go here",
12 | "cookingMethod": "Shake",
13 | "recipeYield": "1 drink",
14 | "recipeCategory": "Drink",
15 | "recipeCuisine": "Cocktail",
16 | "keywords": "",
17 | "recipeIngredient": [
18 | "45 ml - 60 ml Gin"
19 | ],
20 | "image": {
21 | "@type": "ImageObject",
22 | "author": "Exported copyright",
23 | "url": "http://localhost/uploads/tests/non_existing_image.jpg"
24 | }
25 | }
--------------------------------------------------------------------------------
/app/Console/Kernel.php:
--------------------------------------------------------------------------------
1 | command('sanctum:prune-expired --hours=24')->daily();
19 | // $schedule->command('horizon:snapshot')->everyFiveMinutes();
20 | }
21 |
22 | /**
23 | * Register the commands for the application.
24 | *
25 | * @return void
26 | */
27 | protected function commands()
28 | {
29 | $this->load(__DIR__ . '/Commands');
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/database/factories/ExportFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class ExportFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'is_done' => fake()->boolean(),
21 | 'bar_id' => \Kami\Cocktail\Models\Bar::factory(),
22 | 'created_user_id' => \Kami\Cocktail\Models\User::factory(),
23 | 'filename' => fake()->filePath(),
24 | 'created_at' => fake()->dateTime(),
25 | 'updated_at' => fake()->optional()->dateTime(),
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/database/factories/MenuIngredientFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class MenuIngredientFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'category_name' => fake()->name(),
21 | 'sort' => 1,
22 | 'menu_id' => \Kami\Cocktail\Models\Menu::factory(),
23 | 'ingredient_id' => \Kami\Cocktail\Models\Ingredient::factory(),
24 | 'price' => fake()->randomNumber(2),
25 | 'currency' => fake()->currencyCode(),
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Models/CocktailFavorite.php:
--------------------------------------------------------------------------------
1 | */
14 | use HasFactory;
15 |
16 | /**
17 | * @return BelongsTo
18 | */
19 | public function cocktail(): BelongsTo
20 | {
21 | return $this->belongsTo(Cocktail::class);
22 | }
23 |
24 | /**
25 | * @return BelongsTo
26 | */
27 | public function barMembership(): BelongsTo
28 | {
29 | return $this->belongsTo(BarMembership::class);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/Http/Requests/ExportRequest.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | public function rules()
29 | {
30 | return [
31 | 'bar_id' => 'required|integer',
32 | 'type' => ['required', Rule::enum(ExportTypeEnum::class)],
33 | ];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/Http/Requests/RatingRequest.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | public function rules()
27 | {
28 | return [
29 | 'rating' => [
30 | 'required',
31 | 'numeric',
32 | 'integer',
33 | 'min:1',
34 | 'max:5',
35 | ],
36 | ];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Models/BarIngredient.php:
--------------------------------------------------------------------------------
1 | */
14 | use HasFactory;
15 |
16 | public $timestamps = false;
17 |
18 | /**
19 | * @return BelongsTo
20 | */
21 | public function ingredient(): BelongsTo
22 | {
23 | return $this->belongsTo(Ingredient::class);
24 | }
25 |
26 | /**
27 | * @return BelongsTo
28 | */
29 | public function bar(): BelongsTo
30 | {
31 | return $this->belongsTo(Bar::class);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Requests/ImportRequest.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | public function rules()
29 | {
30 | return [
31 | 'source' => 'required',
32 | 'duplicate_actions' => Rule::enum(DuplicateActionsEnum::class),
33 | ];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/database/migrations/2024_06_01_135855_create_complex_ingredients_table.php:
--------------------------------------------------------------------------------
1 | id();
15 | $table->foreignId('main_ingredient_id')->constrained('ingredients')->onDelete('cascade');
16 | $table->foreignId('ingredient_id')->constrained('ingredients')->onDelete('cascade');
17 | });
18 | }
19 |
20 | /**
21 | * Reverse the migrations.
22 | */
23 | public function down(): void
24 | {
25 | Schema::dropIfExists('complex_ingredients');
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/database/migrations/2024_04_01_140655_create_exports_table.php:
--------------------------------------------------------------------------------
1 | id();
15 | $table->foreignId('bar_id');
16 | $table->string('filename');
17 | $table->foreignId('created_user_id');
18 | $table->boolean('is_done')->default(false);
19 | $table->timestamps();
20 | });
21 | }
22 |
23 | /**
24 | * Reverse the migrations.
25 | */
26 | public function down(): void
27 | {
28 | Schema::dropIfExists('exports');
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/app/Models/ValueObjects/Price.php:
--------------------------------------------------------------------------------
1 | money;
18 | }
19 |
20 | public function getPriceAsFloat(): float
21 | {
22 | return $this->money->getAmount()->toFloat();
23 | }
24 |
25 | public function getPriceAsMinor(): int
26 | {
27 | return $this->money->getMinorAmount()->toInt();
28 | }
29 |
30 | public function getFormattedPrice(): string
31 | {
32 | return (string) $this->money;
33 | }
34 |
35 | public function getCurrency(): string
36 | {
37 | return $this->money->getCurrency()->getCurrencyCode();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/Services/Feeds/FeedsClient.php:
--------------------------------------------------------------------------------
1 | get($uri);
23 |
24 | return new Psr7ResponseDecorator($response->toPsrResponse());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/database/migrations/2024_07_07_184922_create_price_categories_table.php:
--------------------------------------------------------------------------------
1 | id();
15 | $table->foreignId('bar_id')->constrained('bars')->onDelete('cascade');
16 | $table->text('name');
17 | $table->string('currency', 3);
18 | $table->text('description')->nullable();
19 | });
20 | }
21 |
22 | /**
23 | * Reverse the migrations.
24 | */
25 | public function down(): void
26 | {
27 | Schema::dropIfExists('price_categories');
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/app/Models/FileToken.php:
--------------------------------------------------------------------------------
1 | $filename,
15 | ];
16 |
17 | $payload = urldecode(http_build_query($payload));
18 | $payload = implode("\n", [$id, $expires->getTimestamp(), $payload]);
19 |
20 | return hash_hmac('sha256', $payload, (string) config('app.key'));
21 | }
22 |
23 | public static function check(string $token, int $id, string $filename, DateTimeImmutable $expires): bool
24 | {
25 | if ((new DateTimeImmutable()) > $expires) {
26 | return false;
27 | }
28 |
29 | return $token === self::generate($id, $filename, $expires);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/Models/UserIngredient.php:
--------------------------------------------------------------------------------
1 | */
14 | use HasFactory;
15 |
16 | public $timestamps = false;
17 |
18 | /**
19 | * @return BelongsTo
20 | */
21 | public function ingredient(): BelongsTo
22 | {
23 | return $this->belongsTo(Ingredient::class);
24 | }
25 |
26 | /**
27 | * @return BelongsTo
28 | */
29 | public function barMembership(): BelongsTo
30 | {
31 | return $this->belongsTo(BarMembership::class);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Controllers/FeedsController.php:
--------------------------------------------------------------------------------
1 | fetch();
22 |
23 | return FeedRecipeResource::collection($feedRecipes);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Models/PriceCategory.php:
--------------------------------------------------------------------------------
1 | */
16 | use HasFactory;
17 | use HasBarAwareScope;
18 |
19 | public $timestamps = false;
20 |
21 | public function getCurrency(): Currency
22 | {
23 | return Currency::of($this->currency);
24 | }
25 |
26 | /**
27 | * @return BelongsTo
28 | */
29 | public function bar(): BelongsTo
30 | {
31 | return $this->belongsTo(Bar::class);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/OpenAPI/ValidationFailedResponse.php:
--------------------------------------------------------------------------------
1 | id();
15 | $table->foreignId('bar_id')->constrained()->onDelete('cascade');
16 | $table->foreignId('ingredient_id')->constrained()->onDelete('cascade');
17 | $table->unique(['bar_id', 'ingredient_id']);
18 | $table->comment('Bar shelf ingredients');
19 | });
20 | }
21 |
22 | /**
23 | * Reverse the migrations.
24 | */
25 | public function down(): void
26 | {
27 | Schema::dropIfExists('bar_ingredients');
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/app/Http/Requests/UpdateUserRequest.php:
--------------------------------------------------------------------------------
1 |
26 | */
27 | public function rules()
28 | {
29 | return [
30 | 'email' => ['required', Rule::unique('users')->ignore($this->user()->id)],
31 | 'name' => 'required',
32 | 'password' => 'confirmed|nullable|min:5',
33 | 'is_shelf_public' => 'boolean',
34 | ];
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/Models/Utensil.php:
--------------------------------------------------------------------------------
1 | */
16 | use HasFactory;
17 | use HasBarAwareScope;
18 |
19 | /**
20 | * @return HasMany
21 | */
22 | public function cocktails(): HasMany
23 | {
24 | return $this->hasMany(Cocktail::class);
25 | }
26 |
27 | /**
28 | * @return BelongsTo
29 | */
30 | public function bar(): BelongsTo
31 | {
32 | return $this->belongsTo(Bar::class);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/database/migrations/2024_05_18_170708_add_total_volume_col_to_glasses.php:
--------------------------------------------------------------------------------
1 | decimal('volume')->nullable();
15 | $table->string('volume_units')->nullable();
16 | });
17 | }
18 |
19 | /**
20 | * Reverse the migrations.
21 | */
22 | public function down(): void
23 | {
24 | Schema::table('glasses', function (Blueprint $table) {
25 | $table->dropColumn('volume');
26 | });
27 | Schema::table('glasses', function (Blueprint $table) {
28 | $table->dropColumn('volume_units');
29 | });
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/app/Console/Commands/RebuildHierarchy.php:
--------------------------------------------------------------------------------
1 | argument('barId');
22 |
23 | try {
24 | $repo->rebuildMaterializedPath($barId);
25 | } catch (\Exception $e) {
26 | $this->error($e->getMessage());
27 |
28 | return Command::FAILURE;
29 | }
30 |
31 | $this->output->success('Done!');
32 |
33 | return Command::SUCCESS;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/Http/Resources/TokenResource.php:
--------------------------------------------------------------------------------
1 |
28 | */
29 | public function toArray($request)
30 | {
31 | return [
32 | 'token' => $this->plainTextToken,
33 | ];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/fixtures/import_collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Test collection",
3 | "description": null,
4 | "cocktails": [
5 | {
6 | "name": "Test 1",
7 | "instructions": "Lorem ipsum 1",
8 | "garnish": "orange twist",
9 | "description": null,
10 | "source": null,
11 | "tags": [],
12 | "glass": "Lowball",
13 | "method": "Stir",
14 | "abv": 29.88,
15 | "images": [],
16 | "ingredients": []
17 | },
18 | {
19 | "name": "Test 2",
20 | "instructions": "Lorem ipsum 2",
21 | "garnish": "orange twist",
22 | "description": null,
23 | "source": null,
24 | "tags": [],
25 | "glass": "Lowball",
26 | "method": "Stir",
27 | "abv": 29.88,
28 | "images": [],
29 | "ingredients": []
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/app/Console/Commands/BarTestEmail.php:
--------------------------------------------------------------------------------
1 | argument('email');
31 |
32 | Mail::to($toEmail)->queue(new TestEmail());
33 |
34 | $this->info('Test email sent to: ' . $toEmail);
35 |
36 | return Command::SUCCESS;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/OpenAPI/NotFoundResponse.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | public function rules()
29 | {
30 | return [
31 | 'file' => 'required|file|mimes:zip|max:1048576', // 1 GB
32 | 'bar_id' => 'required|integer',
33 | 'duplicate_actions' => [Rule::enum(DuplicateActionsEnum::class)]
34 | ];
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/database/migrations/2025_02_26_161635_create_oauth_credentials_table.php:
--------------------------------------------------------------------------------
1 | id();
15 | $table->foreignId('user_id')->constrained()->onDelete('cascade');
16 | $table->string('provider');
17 | $table->string('provider_id');
18 | $table->timestamps();
19 |
20 | $table->unique(['provider', 'user_id'], 'idx_uniq_provider_user_id');
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | */
27 | public function down(): void
28 | {
29 | Schema::dropIfExists('oauth_credentials');
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/config/cors.php:
--------------------------------------------------------------------------------
1 | ['api/*', 'sanctum/csrf-cookie'],
19 |
20 | 'allowed_methods' => ['*'],
21 |
22 | 'allowed_origins' => ['*'],
23 |
24 | 'allowed_origins_patterns' => [],
25 |
26 | 'allowed_headers' => ['*'],
27 |
28 | 'exposed_headers' => [],
29 |
30 | 'max_age' => 0,
31 |
32 | 'supports_credentials' => true,
33 |
34 | ];
35 |
--------------------------------------------------------------------------------
/app/Http/Middleware/CheckUserSubscription.php:
--------------------------------------------------------------------------------
1 | user() && !$request->user()->hasActiveSubscription()) {
22 | return response()->json([
23 | 'error' => 'api_error',
24 | 'message' => 'You can not do that on your current subscription plan!'
25 | ], 400);
26 | }
27 |
28 | return $next($request);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/OpenAPI/NotAuthorizedResponse.php:
--------------------------------------------------------------------------------
1 | float('sugar_g_per_ml')->nullable();
15 | $table->float('acidity')->nullable();
16 | $table->string('distillery')->nullable();
17 | });
18 | }
19 |
20 | /**
21 | * Reverse the migrations.
22 | */
23 | public function down(): void
24 | {
25 | Schema::table('ingredients', function (Blueprint $table) {
26 | $table->dropColumn('sugar_g_per_ml');
27 | $table->dropColumn('acidity');
28 | $table->dropColumn('distillery');
29 | });
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/app/OpenAPI/Schemas/IngredientHierarchy.php:
--------------------------------------------------------------------------------
1 | Gin', property: 'path_to_self', description: 'Path to the current ingredient from the root')]
14 | public string $pathToSelf;
15 |
16 | #[OAT\Property(property: 'parent_ingredient')]
17 | public ?IngredientBasicResource $parentIngredient = null;
18 |
19 | /** @var IngredientBasicResource[] */
20 | #[OAT\Property()]
21 | public array $descendants = [];
22 |
23 | /** @var IngredientBasicResource[] */
24 | #[OAT\Property()]
25 | public array $ancestors = [];
26 |
27 | #[OAT\Property(property: 'root_ingredient_id', description: 'Root ingredient ID')]
28 | public ?string $rootIngredientId = null;
29 | }
30 |
--------------------------------------------------------------------------------
/app/Policies/TagPolicy.php:
--------------------------------------------------------------------------------
1 | isBarAdmin(bar()->id)
18 | || $user->isBarModerator(bar()->id);
19 | }
20 |
21 | public function show(User $user, Tag $tag): bool
22 | {
23 | return $user->hasBarMembership($tag->bar_id);
24 | }
25 |
26 | public function edit(User $user, Tag $tag): bool
27 | {
28 | return $user->isBarAdmin($tag->bar_id)
29 | || $user->isBarModerator($tag->bar_id);
30 | }
31 |
32 | public function delete(User $user, Tag $tag): bool
33 | {
34 | return $user->isBarAdmin($tag->bar_id)
35 | || $user->isBarModerator($tag->bar_id);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/Http/Filters/NoteQueryFilter.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | final class NoteQueryFilter extends QueryBuilder
15 | {
16 | public function __construct()
17 | {
18 | parent::__construct(Note::query());
19 |
20 | $this
21 | ->allowedFilters([
22 | AllowedFilter::callback('cocktail_id', function ($query, $value) {
23 | $query
24 | ->where('noteable_type', \Kami\Cocktail\Models\Cocktail::class)
25 | ->where('noteable_id', $value);
26 | }),
27 | ])
28 | ->defaultSort('created_at')
29 | ->allowedSorts('created_at')
30 | ->where('user_id', $this->request->user()->id);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Policies/GlassPolicy.php:
--------------------------------------------------------------------------------
1 | isBarAdmin(bar()->id)
18 | || $user->isBarModerator(bar()->id);
19 | }
20 |
21 | public function show(User $user, Glass $glass): bool
22 | {
23 | return $user->hasBarMembership($glass->bar_id);
24 | }
25 |
26 | public function edit(User $user, Glass $glass): bool
27 | {
28 | return $user->isBarAdmin($glass->bar_id)
29 | || $user->isBarModerator($glass->bar_id);
30 | }
31 |
32 | public function delete(User $user, Glass $glass): bool
33 | {
34 | return $user->isBarAdmin($glass->bar_id)
35 | || $user->isBarModerator($glass->bar_id);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/database/migrations/2024_08_17_123305_add_nested_set.php:
--------------------------------------------------------------------------------
1 | nestedSet();
15 | // });
16 |
17 | // $bars = \Illuminate\Support\Facades\DB::table('bars')->pluck('id');
18 | // foreach ($bars as $barId) {
19 | // \Kami\Cocktail\Models\IngredientCategory::scoped(['bar_id' => $barId])->fixTree();
20 | // }
21 | }
22 |
23 | /**
24 | * Reverse the migrations.
25 | */
26 | public function down(): void
27 | {
28 | // Schema::table('ingredient_categories', function (Blueprint $table) {
29 | // $table->dropNestedSet();
30 | // });
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/app/Providers/HorizonServiceProvider.php:
--------------------------------------------------------------------------------
1 | in_array($user->email, [
31 | //
32 | ]));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Models/ComplexIngredient.php:
--------------------------------------------------------------------------------
1 | */
15 | use HasFactory;
16 | use HasAuthors;
17 |
18 | public $timestamps = false;
19 |
20 | /**
21 | * @return BelongsTo
22 | */
23 | public function mainIngredient(): BelongsTo
24 | {
25 | return $this->belongsTo(Ingredient::class, 'main_ingredient_id');
26 | }
27 |
28 | /**
29 | * @return BelongsTo
30 | */
31 | public function ingredient(): BelongsTo
32 | {
33 | return $this->belongsTo(Ingredient::class, 'ingredient_id');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/public/vendor/horizon/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "resources/img/favicon.png": {
3 | "file": "favicon.png",
4 | "src": "resources/img/favicon.png",
5 | "integrity": "sha384-tqnRilkeRgqFt3SUYaxuaQs14WOwuU8Gvk3sqRZmnyWZVhr1Kk19Ecr7dFMb4HZo"
6 | },
7 | "resources/js/app.js": {
8 | "file": "app.js",
9 | "name": "app",
10 | "src": "resources/js/app.js",
11 | "isEntry": true,
12 | "css": [
13 | "app.css"
14 | ],
15 | "integrity": "sha384-EV5vlraT2g7leIzueltC7I+UzR7uBT4ndQF5b1G9I+kUrQ4XL0DREuRw/XiU/U3P"
16 | },
17 | "resources/sass/styles-dark.scss": {
18 | "file": "styles-dark.css",
19 | "src": "resources/sass/styles-dark.scss",
20 | "isEntry": true,
21 | "integrity": "sha384-/sLOxh+NTFEdcZ8svIuVTv/lSL65X3QGIXhExXAhntQYWjiez1CQbv4ICbtwRfd8"
22 | },
23 | "resources/sass/styles.scss": {
24 | "file": "styles.css",
25 | "src": "resources/sass/styles.scss",
26 | "isEntry": true,
27 | "integrity": "sha384-4HOmv1E51xgqbUYzCYXaRXPRja5nEho6eq/L/CKs0LlidzTGNTk81VtCAHqLiYSC"
28 | }
29 | }
--------------------------------------------------------------------------------
/app/Models/CocktailMethod.php:
--------------------------------------------------------------------------------
1 | */
17 | use HasFactory;
18 | use HasBarAwareScope;
19 | use HasAuthors;
20 |
21 | /**
22 | * @return HasMany
23 | */
24 | public function cocktails(): HasMany
25 | {
26 | return $this->hasMany(Cocktail::class);
27 | }
28 |
29 | /**
30 | * @return BelongsTo
31 | */
32 | public function bar(): BelongsTo
33 | {
34 | return $this->belongsTo(Bar::class);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/Policies/UtensilPolicy.php:
--------------------------------------------------------------------------------
1 | isBarAdmin(bar()->id)
18 | || $user->isBarModerator(bar()->id);
19 | }
20 |
21 | public function show(User $user, Utensil $utensil): bool
22 | {
23 | return $user->isBarAdmin($utensil->bar_id);
24 | }
25 |
26 | public function edit(User $user, Utensil $utensil): bool
27 | {
28 | return $user->isBarAdmin($utensil->bar_id)
29 | || $user->isBarModerator($utensil->bar_id);
30 | }
31 |
32 | public function delete(User $user, Utensil $utensil): bool
33 | {
34 | return $user->isBarAdmin($utensil->bar_id)
35 | || $user->isBarModerator($utensil->bar_id);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/Services/Auth/RegisterUserService.php:
--------------------------------------------------------------------------------
1 | name = $newUserInfo->name;
20 | $user->password = $newUserInfo->hashedPassword;
21 | $user->email = $newUserInfo->email;
22 | if ($requireConfirmation === false) {
23 | $user->email_verified_at = now();
24 | }
25 | $user->save();
26 |
27 | if ($requireConfirmation === true) {
28 | Mail::to($user)->queue(new ConfirmAccount($user->id, sha1($user->email)));
29 | }
30 |
31 | return $user;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/database/factories/CocktailIngredientSubstituteFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class CocktailIngredientSubstituteFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'amount' => fake()->optional()->numberBetween(1, 60),
21 | 'amount_max' => fake()->optional()->numberBetween(1, 60),
22 | 'units' => fake()->optional()->randomElement(['ml', 'cl', 'oz', 'dashes', 'drops', 'tablespoons', 'teaspoons', 'cups', 'pints', 'quarts']),
23 | 'cocktail_ingredient_id' => \Kami\Cocktail\Models\CocktailIngredient::factory(),
24 | 'ingredient_id' => \Kami\Cocktail\Models\Ingredient::factory(),
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php:
--------------------------------------------------------------------------------
1 | id();
15 | $table->morphs('tokenable');
16 | $table->string('name');
17 | $table->string('token', 64)->unique();
18 | $table->text('abilities')->nullable();
19 | $table->timestamp('last_used_at')->nullable();
20 | $table->timestamp('expires_at')->nullable();
21 | $table->timestamps();
22 | });
23 | }
24 |
25 | /**
26 | * Reverse the migrations.
27 | */
28 | public function down(): void
29 | {
30 | Schema::dropIfExists('personal_access_tokens');
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/database/migrations/2025_02_22_100015_create_menu_ingredients.php:
--------------------------------------------------------------------------------
1 | id();
17 | $table->string('category_name');
18 | $table->integer('sort')->default(0);
19 | $table->foreignId('menu_id')->cascadeOnDelete();
20 | $table->foreignId('ingredient_id')->cascadeOnDelete();
21 | $table->integer('price')->default(0);
22 | $table->string('currency')->nullable();
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | */
29 | public function down(): void
30 | {
31 | Schema::dropIfExists('menu_ingredients');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/app/Http/Middleware/RedirectIfAuthenticated.php:
--------------------------------------------------------------------------------
1 | check()) {
26 | return redirect(RouteServiceProvider::HOME);
27 | }
28 | }
29 |
30 | return $next($request);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Models/Collection.php:
--------------------------------------------------------------------------------
1 | */
15 | use HasFactory;
16 |
17 | protected $casts = [
18 | 'is_bar_shared' => 'boolean',
19 | ];
20 |
21 | /**
22 | * @return BelongsToMany
23 | */
24 | public function cocktails(): BelongsToMany
25 | {
26 | return $this->belongsToMany(Cocktail::class, 'collections_cocktails')->orderBy('name');
27 | }
28 |
29 | /**
30 | * @return BelongsTo
31 | */
32 | public function barMembership(): BelongsTo
33 | {
34 | return $this->belongsTo(BarMembership::class);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/Metrics/TotalBars.php:
--------------------------------------------------------------------------------
1 | DB::table('bars')->select(
16 | DB::raw("(CASE WHEN status IS NULL THEN 'active' ELSE status END) AS bar_status"),
17 | DB::raw('COUNT(*) AS total')
18 | )->groupBy('bar_status')->get()->keyBy('bar_status'));
19 |
20 | foreach (BarStatusEnum::cases() as $status) {
21 | $metric = $this->registry->getOrRegisterGauge(
22 | $this->getDefaultNamespace(),
23 | $status->value . '_bars_total',
24 | 'Total number of ' . $status->value . ' bars'
25 | );
26 | $metric->set($counts[$status->value]->total ?? 0);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Models/Tag.php:
--------------------------------------------------------------------------------
1 | */
16 | use HasFactory;
17 | use HasBarAwareScope;
18 |
19 | public $timestamps = false;
20 |
21 | public $fillable = ['name', 'bar_id'];
22 |
23 | /**
24 | * @return BelongsToMany
25 | */
26 | public function cocktails(): BelongsToMany
27 | {
28 | return $this->belongsToMany(Cocktail::class);
29 | }
30 |
31 | /**
32 | * @return BelongsTo
33 | */
34 | public function bar(): BelongsTo
35 | {
36 | return $this->belongsTo(Bar::class);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Services/Auth/OauthProvider.php:
--------------------------------------------------------------------------------
1 | 'GitHub',
28 | self::Google => 'Google',
29 | self::GitLab => 'GitLab',
30 | self::Authentik => 'Authentik',
31 | self::Authelia => 'Authelia',
32 | self::Keycloak => 'Keycloak',
33 | self::PocketId => 'PocketId',
34 | self::Zitadel => 'Zitadel',
35 | };
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/database/factories/IngredientFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class IngredientFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'name' => fake()->name(),
21 | 'slug' => fake()->slug(),
22 | 'origin' => fake()->country(),
23 | 'description' => fake()->paragraph(),
24 | 'color' => fake()->hexColor(),
25 | 'strength' => fake()->randomFloat(2, 0, 100),
26 | 'created_user_id' => \Kami\Cocktail\Models\User::factory(),
27 | 'bar_id' => \Kami\Cocktail\Models\Bar::factory(),
28 | 'created_at' => fake()->dateTime(),
29 | 'updated_at' => fake()->dateTime(),
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.env.dist:
--------------------------------------------------------------------------------
1 | APP_NAME="Bar Assistant API"
2 | APP_ENV=production
3 | APP_KEY=
4 | APP_DEBUG=false
5 | APP_URL=
6 |
7 | # Database
8 | DB_CONNECTION=sqlite
9 | DB_FOREIGN_KEYS=true
10 |
11 | # Drivers
12 | LOG_CHANNEL=stderr
13 | LOG_LEVEL=warning
14 | CACHE_DRIVER=redis
15 | FILESYSTEM_DISK=local
16 | QUEUE_CONNECTION=sync
17 | SESSION_DRIVER=redis
18 | SESSION_LIFETIME=120
19 |
20 | # Redis
21 | REDIS_HOST=127.0.0.1
22 | REDIS_PASSWORD=null
23 | REDIS_PORT=6379
24 |
25 | # Search
26 | SCOUT_DRIVER=meilisearch
27 | MEILISEARCH_HOST=
28 | MEILISEARCH_KEY=
29 |
30 | # Mail
31 | # MAIL_MAILER=
32 | # MAIL_HOST=
33 | # MAIL_PORT=
34 | # MAIL_USERNAME=
35 | # MAIL_PASSWORD=
36 | # MAIL_ENCRYPTION=
37 | # MAIL_RESET_URL=
38 | # MAIL_CONFIRM_URL=
39 | MAIL_FROM_ADDRESS="no-reply@barassistant.app"
40 | MAIL_FROM_NAME="Bar Assistant"
41 | MAIL_REQUIRE_CONFIRMATION=false
42 |
43 | # Billing
44 | # ENABLE_BILLING=
45 | # BILLING_PRODUCT_PRICES=
46 | # MAX_USER_BARS=
47 | # CASHIER_CURRENCY=
48 | # PADDLE_SANDBOX=
49 | # PADDLE_VENDOR_ID=
50 | # PADDLE_VENDOR_AUTH_CODE=
51 | # PADDLE_AUTH_CODE=
52 |
--------------------------------------------------------------------------------
/app/Console/Commands/BarClearMetrics.php:
--------------------------------------------------------------------------------
1 | error('Metrics are not enabled');
31 |
32 | return;
33 | }
34 |
35 | /** @var CollectorRegistry */
36 | $registry = resolve(CollectorRegistry::class);
37 | $registry->wipeStorage();
38 |
39 | $this->info('Metrics storage cleared!');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/fixtures/external/recipe.yml:
--------------------------------------------------------------------------------
1 | recipe:
2 | _id: gin-and-tonic_1
3 | name: 'Gin and Tonic'
4 | instructions: 'Cocktail instructions go here'
5 | created_at: '2020-01-01T12:00:00+00:00'
6 | updated_at: '2021-01-01T12:00:00+00:00'
7 | description: 'Cocktail description goes here'
8 | source: 'https://barassistant.app'
9 | garnish: 'Straw and lemon wheel'
10 | abv: 32.3
11 | tags: { }
12 | glass: Highball
13 | method: Shake
14 | utensils: { }
15 | images:
16 | -
17 | uri: 'http://localhost/uploads/tests/non_existing_image.jpg'
18 | sort: 1
19 | placeholder_hash: null
20 | copyright: 'Exported copyright'
21 | ingredients:
22 | -
23 | _id: gin_1
24 | amount: 45.0
25 | units: ml
26 | optional: false
27 | is_specified: false
28 | amount_max: 60.0
29 | note: 'Ingredient note'
30 | substitutes: { }
31 | sort: 1
32 | ingredients:
33 | -
34 | _id: gin_1
35 | name: Gin
36 | strength: 40.0
37 | description: 'Test description'
38 | origin: London
39 | category: null
40 |
--------------------------------------------------------------------------------
/app/Http/Filters/CollectionQueryFilter.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | final class CollectionQueryFilter extends QueryBuilder
15 | {
16 | public function __construct()
17 | {
18 | parent::__construct(ItemsCollection::query());
19 |
20 | $barMembership = $this->request->user()->getBarMembership(bar()->id);
21 |
22 | $this
23 | ->allowedFilters([
24 | AllowedFilter::exact('id'),
25 | AllowedFilter::partial('name'),
26 | AllowedFilter::exact('cocktail_id', 'cocktails.id'),
27 | ])
28 | ->defaultSort('name')
29 | ->allowedSorts('name', 'created_at')
30 | ->allowedIncludes('cocktails')
31 | ->where('bar_membership_id', $barMembership->id);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Policies/CocktailMethodPolicy.php:
--------------------------------------------------------------------------------
1 | isBarAdmin(bar()->id)
18 | || $user->isBarModerator(bar()->id);
19 | }
20 |
21 | public function show(User $user, CocktailMethod $method): bool
22 | {
23 | return $user->hasBarMembership($method->bar_id);
24 | }
25 |
26 | public function edit(User $user, CocktailMethod $method): bool
27 | {
28 | return $user->isBarAdmin($method->bar_id)
29 | || $user->isBarModerator($method->bar_id);
30 | }
31 |
32 | public function delete(User $user, CocktailMethod $method): bool
33 | {
34 | return $user->isBarAdmin($method->bar_id)
35 | || $user->isBarModerator($method->bar_id);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/Policies/CalculatorPolicy.php:
--------------------------------------------------------------------------------
1 | isBarAdmin(bar()->id)
18 | || $user->isBarModerator(bar()->id);
19 | }
20 |
21 | public function show(User $user, Calculator $calculator): bool
22 | {
23 | return $user->hasBarMembership($calculator->bar_id);
24 | }
25 |
26 | public function edit(User $user, Calculator $calculator): bool
27 | {
28 | return $user->isBarAdmin($calculator->bar_id)
29 | || $user->isBarModerator($calculator->bar_id);
30 | }
31 |
32 | public function delete(User $user, Calculator $calculator): bool
33 | {
34 | return $user->isBarAdmin($calculator->bar_id)
35 | || $user->isBarModerator($calculator->bar_id);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/database/factories/CocktailIngredientFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class CocktailIngredientFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | 'amount' => fake()->numberBetween(1, 60),
21 | 'amount_max' => fake()->optional()->numberBetween(1, 60),
22 | 'sort' => 1,
23 | 'optional' => fake()->boolean(),
24 | 'note' => fake()->sentence(),
25 | 'units' => fake()->randomElement(['ml', 'cl', 'oz', 'dashes', 'drops', 'tablespoons', 'teaspoons', 'cups', 'pints', 'quarts']),
26 | 'cocktail_id' => \Kami\Cocktail\Models\Cocktail::factory(),
27 | 'ingredient_id' => \Kami\Cocktail\Models\Ingredient::factory(),
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/Http/Resources/UserBasicResource.php:
--------------------------------------------------------------------------------
1 |
29 | */
30 | public function toArray($request)
31 | {
32 | return [
33 | 'id' => $this->id,
34 | 'name' => $this->name,
35 | ];
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/database/migrations/2024_07_07_185728_create_ingredient_prices_table.php:
--------------------------------------------------------------------------------
1 | id();
15 | $table->foreignId('price_category_id')->constrained('price_categories')->onDelete('cascade');
16 | $table->foreignId('ingredient_id')->constrained('ingredients')->onDelete('cascade');
17 | $table->integer('price');
18 | $table->decimal('amount');
19 | $table->text('units');
20 | $table->text('description')->nullable();
21 | $table->timestamps();
22 | });
23 | }
24 |
25 | /**
26 | * Reverse the migrations.
27 | */
28 | public function down(): void
29 | {
30 | Schema::dropIfExists('ingredient_prices');
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/database/factories/CalculatorBlockFactory.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class CalculatorBlockFactory extends Factory
13 | {
14 | /**
15 | * Define the model's default state.
16 | *
17 | * @return array
18 | */
19 | public function definition()
20 | {
21 | return [
22 | 'label' => fake()->name(),
23 | 'type' => fake()->randomElement(CalculatorBlockTypeEnum::class),
24 | 'description' => fake()->optional()->paragraph(),
25 | 'variable_name' => Str::slug(fake()->userName()),
26 | 'value' => '1 + 2',
27 | 'sort' => 1,
28 | 'settings' => json_encode(['suffix' => 'suf', 'prefix' => 'pre']),
29 | 'calculator_id' => \Kami\Cocktail\Models\Calculator::factory(),
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Services/Image/ImageHashingService.php:
--------------------------------------------------------------------------------
1 | thumbnail_image($width, ['height' => $height, 'crop' => 'centre']);
15 |
16 | if ($image->bands < 4) {
17 | $image = $image->bandjoin(255);
18 | }
19 |
20 | $pixels = $image->writeToArray();
21 | $rgbaPixels = [];
22 |
23 | for ($i = 0; $i < count($pixels); $i += 4) {
24 | $rgbaPixels[] = $pixels[$i];
25 | $rgbaPixels[] = $pixels[$i + 1];
26 | $rgbaPixels[] = $pixels[$i + 2];
27 | $rgbaPixels[] = $pixels[$i + 3];
28 | }
29 |
30 | $hash = Thumbhash::RGBAToHash($width, $height, $rgbaPixels);
31 | $key = Thumbhash::convertHashToString($hash);
32 |
33 | return $key;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/resources/views/swagger.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Docs
6 |
7 |
8 |
9 |
10 |
11 | Loading....
12 |
13 |
14 |
15 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Karlo Mikuš
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/Http/Resources/UserShoppingListResource.php:
--------------------------------------------------------------------------------
1 |
29 | */
30 | public function toArray($request)
31 | {
32 | return [
33 | 'ingredient' => new IngredientBasicResource($this->ingredient),
34 | 'quantity' => $this->quantity,
35 | ];
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/OpenAPI/Schemas/CalculatorRequest.php:
--------------------------------------------------------------------------------
1 | $blocks
14 | */
15 | public function __construct(
16 | #[OAT\Property()]
17 | public string $name,
18 | #[OAT\Property(items: new OAT\Items(type: CalculatorBlockRequest::class))]
19 | public array $blocks,
20 | #[OAT\Property()]
21 | public ?string $description = null,
22 | ) {
23 | }
24 |
25 | /**
26 | * @param array $source
27 | */
28 | public static function fromArray(array $source): self
29 | {
30 | $blocks = [];
31 | foreach ($source['blocks'] ?? [] as $block) {
32 | $blocks[] = CalculatorBlockRequest::fromArray($block);
33 | }
34 |
35 | return new self(
36 | $source['name'],
37 | $blocks,
38 | $source['description'] ?? null,
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/database/factories/IngredientPriceFactory.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class IngredientPriceFactory extends Factory
12 | {
13 | /**
14 | * Define the model's default state.
15 | *
16 | * @return array
17 | */
18 | public function definition()
19 | {
20 | return [
21 | 'price_category_id' => \Kami\Cocktail\Models\PriceCategory::factory(),
22 | 'ingredient_id' => \Kami\Cocktail\Models\Ingredient::factory(),
23 | 'price' => fake()->randomNumber(2) * 100,
24 | 'amount' => fake()->randomeLement([125, 250, 325, 500, 700, 750, 1000]),
25 | 'units' => fake()->randomElement(Units::class),
26 | 'description' => fake()->optional()->text(),
27 | 'created_at' => fake()->dateTime(),
28 | 'updated_at' => fake()->optional()->dateTime(),
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/Rules/SubscriberImagesCount.php:
--------------------------------------------------------------------------------
1 | authenticatedUser->hasActiveSubscription() && (count($value) > $this->maxSubscriberImages)) {
33 | $fail('Total images must be less than ' . $this->maxSubscriberImages);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/ZipUtils.php:
--------------------------------------------------------------------------------
1 | unzipDisk = Storage::disk('temp');
21 | $this->dirName = Str::random(8) . '/';
22 | }
23 |
24 | public function unzip(string $filename): void
25 | {
26 | $zip = new ZipArchive();
27 | if ($zip->open($filename) !== true) {
28 | throw new Exception(sprintf('Unable to open zip file: "%s"', $filename));
29 | }
30 | $zip->extractTo($this->unzipDisk->path($this->dirName));
31 | $zip->close();
32 | }
33 |
34 | public function getDirName(): string
35 | {
36 | return $this->dirName;
37 | }
38 |
39 | public function cleanup(): void
40 | {
41 | $this->unzipDisk->deleteDirectory($this->dirName);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/Console/Commands/BarSearchRefresh.php:
--------------------------------------------------------------------------------
1 | option('clear')) {
34 | $this->info('Clearing index and syncing...');
35 | } else {
36 | $this->info('Syncing search index...');
37 | }
38 |
39 | RefreshSearchIndex::dispatch((bool) $this->option('clear'));
40 |
41 | return Command::SUCCESS;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | 'This is your Bar Assistant instance. Checkout /docs to see documentation.
If you are trying to make a request to the API, make sure you are using the correct endpoint (e.g., /api/cocktails).
Also make sure you are using all the required headers: Accept, Authorization.');
19 |
20 | Route::get('/docs', fn () => view('elements'));
21 |
22 | if (config('bar-assistant.metrics.enabled') === true) {
23 | Route::get('/metrics', [MetricsController::class, 'index'])->name('metrics')->middleware(CheckMetricsAccess::class);
24 | }
25 |
--------------------------------------------------------------------------------
/app/Policies/UserPolicy.php:
--------------------------------------------------------------------------------
1 | isBarAdmin(bar()->id)
17 | || $user->isBarModerator(bar()->id);
18 | }
19 |
20 | public function create(User $user): bool
21 | {
22 | return $user->hasActiveSubscription()
23 | && ($user->isBarAdmin(bar()->id) || $user->isBarModerator(bar()->id));
24 | }
25 |
26 | public function show(User $user, User $model): bool
27 | {
28 | return $user->isBarAdmin(bar()->id)
29 | || $user->isBarModerator(bar()->id);
30 | }
31 |
32 | public function edit(User $user, User $model): bool
33 | {
34 | return $user->isBarAdmin(bar()->id)
35 | || $user->isBarModerator(bar()->id);
36 | }
37 |
38 | public function delete(User $user, User $model): bool
39 | {
40 | return $user->id === $model->id;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/Console/Commands/BarAnon.php:
--------------------------------------------------------------------------------
1 | email = 'email' . $user->id . '@example.com';
38 | $user->name = 'User ' . $user->id;
39 | $user->password = Hash::make('Test12345');
40 | $user->save();
41 | }
42 |
43 | $this->output->success('Done!');
44 |
45 | return Command::SUCCESS;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------