├── 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 | 4 | 5 | 8 | 9 | 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 | 4 | 5 | @if (trim($slot) === 'Laravel') 6 | 7 | @else 8 | {{ $slot }}
9 | All-in-one solution for managing your home bar 10 | @endif 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/fixtures/external/markdown.md: -------------------------------------------------------------------------------- 1 | # Gin and Tonic 2 | [Recipe source](https://barassistant.app) 3 | Cocktail description goes here 4 | 5 | ![Exported copyright](http://localhost/uploads/tests/non_existing_image.jpg) 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 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 | --------------------------------------------------------------------------------