From cbd923ef08cd0f9a8b22941e3f72178bd6703c42 Mon Sep 17 00:00:00 2001 From: Mazarin Date: Tue, 26 Jul 2022 18:42:12 -0400 Subject: [PATCH] feat: documents (monicahq/chandler#159) --- .env.example | 12 ++ .../EnvVariablesNotSetException.php | 9 + app/Helpers/FileHelper.php | 25 +++ app/Helpers/StorageHelper.php | 31 +++ app/Jobs/SetupAccount.php | 17 +- app/Models/Account.php | 19 ++ app/Models/Contact.php | 10 + app/Models/File.php | 57 +++++ app/Models/Module.php | 1 + app/Providers/EventServiceProvider.php | 5 + composer.json | 1 + composer.lock | 175 +++++++++++++-- config/monica.php | 16 ++ config/services.php | 11 +- database/factories/AccountFactory.php | 4 +- database/factories/FileFactory.php | 36 ++++ ...013_04_25_132851_create_accounts_table.php | 1 + .../2022_07_24_002342_create_files_table.php | 42 ++++ .../ManageContact/Services/DestroyContact.php | 10 + .../Web/ViewHelpers/ContactShowViewHelper.php | 5 + .../ManageDocuments/Events/FileDeleted.php | 25 +++ .../Listeners/DeleteFileInStorage.php | 84 ++++++++ .../Services/DestroyDocument.php | 74 +++++++ .../ManageDocuments/Services/UploadFile.php | 105 +++++++++ .../ContactModuleDocumentController.php | 57 +++++ .../ViewHelpers/ModuleDocumentsViewHelper.php | 51 +++++ .../CancelAccount/Services/CancelAccount.php | 22 +- .../CreateAccount/Services/CreateAccount.php | 4 +- .../ViewHelpers/SettingsIndexViewHelper.php | 3 + .../Controllers/AccountStorageController.php | 20 ++ .../ViewHelpers/StorageIndexViewHelper.php | 73 +++++++ .../Web/Controllers/VaultFileController.php | 96 +++++++++ .../ViewHelpers/VaultFileIndexViewHelper.php | 93 ++++++++ .../ManageVault/Services/DestroyVault.php | 14 ++ .../Web/ViewHelpers/VaultIndexViewHelper.php | 3 + lang/en/app.php | 8 +- lang/en/contact.php | 13 ++ lang/en/settings.php | 12 ++ lang/en/vault.php | 22 +- lang/fr/app.php | 8 +- lang/fr/contact.php | 12 ++ lang/fr/settings.php | 12 ++ lang/fr/vault.php | 12 ++ package.json | 1 + phpstan.neon | 1 + resources/css/app.css | 4 + resources/js/Pages/Settings/Index.vue | 6 + resources/js/Pages/Settings/Storage/Index.vue | 116 ++++++++++ resources/js/Pages/Vault/Contact/Show.vue | 9 + resources/js/Pages/Vault/Files/Index.vue | 204 ++++++++++++++++++ resources/js/Shared/Layout.vue | 7 + resources/js/Shared/Modules/Documents.vue | 198 +++++++++++++++++ routes/web.php | 20 ++ .../Services/DestroyContactTest.php | 12 ++ .../Services/DestroyDocumentTest.php | 150 +++++++++++++ .../Services/UploadFileTest.php | 156 ++++++++++++++ .../ModuleDocumentsViewHelperTest.php | 75 +++++++ .../Services/CancelAccountTest.php | 18 +- .../Services/CreateAccountTest.php | 2 + .../SettingsIndexViewHelperTest.php | 3 + .../StorageIndexViewHelperTest.php | 77 +++++++ .../VaultFileIndexViewHelperTest.php | 75 +++++++ .../ManageVault/Services/DestroyVaultTest.php | 18 +- .../ViewHelpers/VaultIndexViewHelperTest.php | 1 + tests/Unit/Helpers/FileHelperTest.php | 25 +++ tests/Unit/Helpers/StorageHelperTest.php | 39 ++++ tests/Unit/Models/AccountTest.php | 12 ++ tests/Unit/Models/ContactTest.php | 12 ++ tests/Unit/Models/FileTest.php | 20 ++ yarn.lock | 22 +- 70 files changed, 2552 insertions(+), 41 deletions(-) create mode 100644 app/Exceptions/EnvVariablesNotSetException.php create mode 100644 app/Helpers/FileHelper.php create mode 100644 app/Helpers/StorageHelper.php create mode 100644 app/Models/File.php create mode 100644 config/monica.php create mode 100644 database/factories/FileFactory.php create mode 100644 database/migrations/2022_07_24_002342_create_files_table.php create mode 100644 domains/Contact/ManageDocuments/Events/FileDeleted.php create mode 100644 domains/Contact/ManageDocuments/Listeners/DeleteFileInStorage.php create mode 100644 domains/Contact/ManageDocuments/Services/DestroyDocument.php create mode 100644 domains/Contact/ManageDocuments/Services/UploadFile.php create mode 100644 domains/Contact/ManageDocuments/Web/Controllers/ContactModuleDocumentController.php create mode 100644 domains/Contact/ManageDocuments/Web/ViewHelpers/ModuleDocumentsViewHelper.php create mode 100644 domains/Settings/ManageStorage/Web/Controllers/AccountStorageController.php create mode 100644 domains/Settings/ManageStorage/Web/ViewHelpers/StorageIndexViewHelper.php create mode 100644 domains/Vault/ManageFiles/Web/Controllers/VaultFileController.php create mode 100644 domains/Vault/ManageFiles/Web/ViewHelpers/VaultFileIndexViewHelper.php create mode 100644 resources/js/Pages/Settings/Storage/Index.vue create mode 100644 resources/js/Pages/Vault/Files/Index.vue create mode 100644 resources/js/Shared/Modules/Documents.vue create mode 100644 tests/Unit/Domains/Contact/ManageDocuments/Services/DestroyDocumentTest.php create mode 100644 tests/Unit/Domains/Contact/ManageDocuments/Services/UploadFileTest.php create mode 100644 tests/Unit/Domains/Contact/ManageDocuments/Web/ViewHelpers/ModuleDocumentsViewHelperTest.php create mode 100644 tests/Unit/Domains/Settings/ManageStorage/Web/ViewHelpers/StorageIndexViewHelperTest.php create mode 100644 tests/Unit/Domains/Vault/ManageFiles/Web/ViewHelpers/VaultFileIndexViewHelperTest.php create mode 100644 tests/Unit/Helpers/FileHelperTest.php create mode 100644 tests/Unit/Helpers/StorageHelperTest.php create mode 100644 tests/Unit/Models/FileTest.php diff --git a/.env.example b/.env.example index 838583efe51..61119763c45 100644 --- a/.env.example +++ b/.env.example @@ -60,3 +60,15 @@ MEILISEARCH_KEY= TELEGRAM_BOT_TOKEN= TELEGRAM_BOT_URL= TELEGRAM_BOT_WEBHOOK_URL= + +# Default storage limit for accounts on this instance, in Mb +# 0 = unlimited +DEFAULT_STORAGE_LIMIT=50 + +# API key for uploading files +# We use Uploadcare (https://uploadcare.com) to upload and store all user +# generated files. +# Uploadcare is GDPR and all privacy laws compliant. +# It also provides a generous free plan. +UPLOADCARE_PUBLIC_KEY= +UPLOADCARE_PRIVATE_KEY= diff --git a/app/Exceptions/EnvVariablesNotSetException.php b/app/Exceptions/EnvVariablesNotSetException.php new file mode 100644 index 00000000000..5727a21e5c9 --- /dev/null +++ b/app/Exceptions/EnvVariablesNotSetException.php @@ -0,0 +1,9 @@ + 0.9) { + $bytes = $bytes / $step; + $i++; + } + + return round($bytes, 2) . $units[$i]; + } +} diff --git a/app/Helpers/StorageHelper.php b/app/Helpers/StorageHelper.php new file mode 100644 index 00000000000..4e6f61e99f1 --- /dev/null +++ b/app/Helpers/StorageHelper.php @@ -0,0 +1,31 @@ +vaults()->select('id')->get()->pluck('id')->toArray(); + $contactIds = Contact::whereIn('vault_id', $vaultIds)->select('id')->get()->pluck('id')->toArray(); + + $totalSizeInBytes = File::whereIn('contact_id', $contactIds)->sum('size'); + + $accountLimit = $account->storage_limit_in_mb * 1024 * 1024; + + return $totalSizeInBytes < $accountLimit; + } +} diff --git a/app/Jobs/SetupAccount.php b/app/Jobs/SetupAccount.php index dc52aeb05e7..871d5031f40 100644 --- a/app/Jobs/SetupAccount.php +++ b/app/Jobs/SetupAccount.php @@ -4,7 +4,6 @@ use App\Models\Currency; use App\Models\Emotion; -use App\Models\Information; use App\Models\LifeEventCategory; use App\Models\LifeEventType; use App\Models\Module; @@ -427,6 +426,22 @@ private function addTemplatePageInformation(): void 'can_be_deleted' => true, ]); + // Documents + $module = (new CreateModule())->execute([ + 'account_id' => $this->user->account_id, + 'author_id' => $this->user->id, + 'name' => trans('app.module_documents'), + 'type' => Module::TYPE_DOCUMENTS, + 'can_be_deleted' => false, + ]); + (new AssociateModuleToTemplatePage())->execute([ + 'account_id' => $this->user->account_id, + 'author_id' => $this->user->id, + 'template_id' => $this->template->id, + 'template_page_id' => $templatePageInformation->id, + 'module_id' => $module->id, + ]); + // Notes $module = (new CreateModule())->execute([ 'account_id' => $this->user->account_id, diff --git a/app/Models/Account.php b/app/Models/Account.php index 77e5f608b00..045b6caad9b 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -11,6 +11,15 @@ class Account extends Model { use HasFactory; + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'storage_limit_in_mb', + ]; + /** * Get the users associated with the account. * @@ -182,4 +191,14 @@ public function giftStates(): HasMany { return $this->hasMany(GiftState::class); } + + /** + * Get the vaults associated with the account. + * + * @return HasMany + */ + public function vaults(): HasMany + { + return $this->hasMany(Vault::class); + } } diff --git a/app/Models/Contact.php b/app/Models/Contact.php index 665d0643dd6..5e6db2d8612 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -313,6 +313,16 @@ public function goals(): HasMany return $this->hasMany(Goal::class); } + /** + * Get the files associated with the contact. + * + * @return HasMany + */ + public function files(): HasMany + { + return $this->hasMany(File::class); + } + /** * Get the groups associated with the contact. * diff --git a/app/Models/File.php b/app/Models/File.php new file mode 100644 index 00000000000..76194800274 --- /dev/null +++ b/app/Models/File.php @@ -0,0 +1,57 @@ + FileDeleted::class, + ]; + + /** + * Get the contact associated with the file. + * + * @return BelongsTo + */ + public function contact(): BelongsTo + { + return $this->belongsTo(Contact::class); + } +} diff --git a/app/Models/Module.php b/app/Models/Module.php index 666ff93a00f..c60ce018e62 100644 --- a/app/Models/Module.php +++ b/app/Models/Module.php @@ -34,6 +34,7 @@ class Module extends Model public const TYPE_ADDRESSES = 'addresses'; public const TYPE_GROUPS = 'groups'; public const TYPE_CONTACT_INFORMATION = 'contact_information'; + public const TYPE_DOCUMENTS = 'documents'; /** * The attributes that are mass assignable. diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 30c0ac9ef8b..3695decace3 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,6 +2,8 @@ namespace App\Providers; +use App\Contact\ManageDocuments\Events\FileDeleted; +use App\Contact\ManageDocuments\Listeners\DeleteFileInStorage; use App\Listeners\LocaleUpdatedListener; use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Listeners\SendEmailVerificationNotification; @@ -23,6 +25,9 @@ class EventServiceProvider extends ServiceProvider LocaleUpdated::class => [ LocaleUpdatedListener::class, ], + FileDeleted::class => [ + DeleteFileInStorage::class, + ], ]; /** diff --git a/composer.json b/composer.json index f89b1cb0f5c..f4b1b810169 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "laravel/tinker": "^2.5", "meilisearch/meilisearch-php": "^0.23.2", "tightenco/ziggy": "^1.0", + "uploadcare/uploadcare-php": "^3.0", "vluzrmos/language-detector": "^2.3" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 577b691663e..e950a834a97 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7b49550f8e7a6f67670354298f2dfbfb", + "content-hash": "98888cc84726bec6f201d5cbe7d711c6", "packages": [ { "name": "asm89/stack-cors", @@ -1989,16 +1989,16 @@ }, { "name": "league/flysystem", - "version": "3.1.1", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "1a941703dfb649f9b821e7bc425e782f576a805e" + "reference": "ed0ecc7f9b5c2f4a9872185846974a808a3b052a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1a941703dfb649f9b821e7bc425e782f576a805e", - "reference": "1a941703dfb649f9b821e7bc425e782f576a805e", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/ed0ecc7f9b5c2f4a9872185846974a808a3b052a", + "reference": "ed0ecc7f9b5c2f4a9872185846974a808a3b052a", "shasum": "" }, "require": { @@ -2059,7 +2059,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.1.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.2.0" }, "funding": [ { @@ -2075,7 +2075,7 @@ "type": "tidelift" } ], - "time": "2022-07-18T09:59:40+00:00" + "time": "2022-07-26T07:26:36+00:00" }, { "name": "league/mime-type-detection", @@ -2202,16 +2202,16 @@ }, { "name": "monolog/monolog", - "version": "2.7.0", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "5579edf28aee1190a798bfa5be8bc16c563bd524" + "reference": "720488632c590286b88b80e62aa3d3d551ad4a50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/5579edf28aee1190a798bfa5be8bc16c563bd524", - "reference": "5579edf28aee1190a798bfa5be8bc16c563bd524", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/720488632c590286b88b80e62aa3d3d551ad4a50", + "reference": "720488632c590286b88b80e62aa3d3d551ad4a50", "shasum": "" }, "require": { @@ -2231,11 +2231,10 @@ "guzzlehttp/psr7": "^2.2", "mongodb/mongodb": "^1.8", "php-amqplib/php-amqplib": "~2.4 || ^3", - "php-console/php-console": "^3.1.3", "phpspec/prophecy": "^1.15", "phpstan/phpstan": "^0.12.91", "phpunit/phpunit": "^8.5.14", - "predis/predis": "^1.1", + "predis/predis": "^1.1 || ^2.0", "rollbar/rollbar": "^1.3 || ^2 || ^3", "ruflin/elastica": "^7", "swiftmailer/swiftmailer": "^5.3|^6.0", @@ -2255,7 +2254,6 @@ "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", - "php-console/php-console": "Allow sending log messages to Google Chrome", "rollbar/rollbar": "Allow sending log messages to Rollbar", "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, @@ -2290,7 +2288,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/2.7.0" + "source": "https://github.com/Seldaek/monolog/tree/2.8.0" }, "funding": [ { @@ -2302,7 +2300,7 @@ "type": "tidelift" } ], - "time": "2022-06-09T08:59:12+00:00" + "time": "2022-07-24T11:55:47+00:00" }, { "name": "nesbot/carbon", @@ -5396,6 +5394,88 @@ ], "time": "2022-05-24T11:49:31+00:00" }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "a41886c1c81dc075a09c71fe6db5b9d68c79de23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/a41886c1c81dc075a09c71fe6db5b9d68c79de23", + "reference": "a41886c1c81dc075a09c71fe6db5b9d68c79de23", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, { "name": "symfony/process", "version": "v6.1.0", @@ -6100,6 +6180,69 @@ }, "time": "2021-12-08T09:12:39+00:00" }, + { + "name": "uploadcare/uploadcare-php", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/uploadcare/uploadcare-php.git", + "reference": "24a275ed8b13d85f84c75da3d9af7b36d9ad44c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/uploadcare/uploadcare-php/zipball/24a275ed8b13d85f84c75da3d9af7b36d9ad44c5", + "reference": "24a275ed8b13d85f84c75da3d9af7b36d9ad44c5", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5||^7", + "guzzlehttp/psr7": "^1.6||^2", + "php": ">=7.1 || >=8.0", + "symfony/polyfill-uuid": "^1.17" + }, + "require-dev": { + "fakerphp/faker": "^1.9", + "friendsofphp/php-cs-fixer": "^2.16", + "phpunit/phpunit": "^5.7||^7||^9", + "symfony/dotenv": "^3.4", + "symfony/var-dumper": "^3.4||^5", + "vimeo/psalm": "^4.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Uploadcare\\": "./src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Uploadcare, Inc", + "email": "hello@uploadcare.com" + } + ], + "description": "Uploadcare PHP integration handles uploads and further operations with files by wrapping Upload and REST APIs.", + "homepage": "https://uploadcare.com", + "keywords": [ + "cdn", + "cloud", + "files", + "image management", + "upload", + "uploadcare" + ], + "support": { + "email": "help@uploadcare.com", + "issues": "https://github.com/uploadcare/uploadcare-php/issues", + "source": "https://github.com/uploadcare/uploadcare-php/tree/v3.2.4" + }, + "time": "2022-07-22T10:47:48+00:00" + }, { "name": "vlucas/phpdotenv", "version": "v5.4.1", diff --git a/config/monica.php b/config/monica.php new file mode 100644 index 00000000000..980563ea427 --- /dev/null +++ b/config/monica.php @@ -0,0 +1,16 @@ + env('DEFAULT_STORAGE_LIMIT', 50), +]; diff --git a/config/services.php b/config/services.php index 720277d66a6..bbac0acbecd 100644 --- a/config/services.php +++ b/config/services.php @@ -24,16 +24,15 @@ 'token' => env('POSTMARK_TOKEN'), ], - 'ses' => [ - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), - ], - 'telegram-bot-api' => [ 'token' => env('TELEGRAM_BOT_TOKEN', null), 'bot_url' => env('TELEGRAM_BOT_URL'), 'webhook' => env('TELEGRAM_BOT_WEBHOOK_URL'), ], + 'uploadcare' => [ + 'public_key' => env('UPLOADCARE_PUBLIC_KEY', null), + 'private_key' => env('UPLOADCARE_PRIVATE_KEY', null), + ], + ]; diff --git a/database/factories/AccountFactory.php b/database/factories/AccountFactory.php index 916a25d78ea..e513c8290ae 100644 --- a/database/factories/AccountFactory.php +++ b/database/factories/AccountFactory.php @@ -21,6 +21,8 @@ class AccountFactory extends Factory */ public function definition() { - return []; + return [ + 'storage_limit_in_mb' => 10, + ]; } } diff --git a/database/factories/FileFactory.php b/database/factories/FileFactory.php new file mode 100644 index 00000000000..38be2356c85 --- /dev/null +++ b/database/factories/FileFactory.php @@ -0,0 +1,36 @@ + Contact::factory(), + 'uuid' => $this->faker->uuid, + 'original_url' => $this->faker->url, + 'cdn_url' => $this->faker->url, + 'name' => $this->faker->name, + 'mime_type' => 'avatar', + 'type' => 'avatar', + 'size' => $this->faker->numberBetween(), + ]; + } +} diff --git a/database/migrations/2013_04_25_132851_create_accounts_table.php b/database/migrations/2013_04_25_132851_create_accounts_table.php index 5f176d1909d..0c3d1624951 100644 --- a/database/migrations/2013_04_25_132851_create_accounts_table.php +++ b/database/migrations/2013_04_25_132851_create_accounts_table.php @@ -16,6 +16,7 @@ public function up() Schema::create('accounts', function (Blueprint $table) { $table->id(); + $table->integer('storage_limit_in_mb')->default(0); $table->timestamps(); }); } diff --git a/database/migrations/2022_07_24_002342_create_files_table.php b/database/migrations/2022_07_24_002342_create_files_table.php new file mode 100644 index 00000000000..1f0aede3466 --- /dev/null +++ b/database/migrations/2022_07_24_002342_create_files_table.php @@ -0,0 +1,42 @@ +id(); + $table->unsignedBigInteger('contact_id'); + $table->string('uuid'); + $table->string('original_url')->nullable(); + $table->string('cdn_url')->nullable(); + $table->string('mime_type'); + $table->string('name'); + $table->string('type'); + $table->integer('size'); + $table->timestamps(); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('files'); + } +}; diff --git a/domains/Contact/ManageContact/Services/DestroyContact.php b/domains/Contact/ManageContact/Services/DestroyContact.php index 00ab63f7335..fdb8738734f 100644 --- a/domains/Contact/ManageContact/Services/DestroyContact.php +++ b/domains/Contact/ManageContact/Services/DestroyContact.php @@ -54,9 +54,19 @@ public function execute(array $data): void throw new CantBeDeletedException(); } + $this->destroyFiles(); + $this->contact->delete(); } + private function destroyFiles(): void + { + $files = $this->contact->files; + foreach ($files as $file) { + $file->delete(); + } + } + private function log(): void { CreateAuditLog::dispatch([ diff --git a/domains/Contact/ManageContact/Web/ViewHelpers/ContactShowViewHelper.php b/domains/Contact/ManageContact/Web/ViewHelpers/ContactShowViewHelper.php index 6248a11e787..cc82c087cd5 100644 --- a/domains/Contact/ManageContact/Web/ViewHelpers/ContactShowViewHelper.php +++ b/domains/Contact/ManageContact/Web/ViewHelpers/ContactShowViewHelper.php @@ -9,6 +9,7 @@ use App\Contact\ManageContactImportantDates\Web\ViewHelpers\ModuleImportantDatesViewHelper; use App\Contact\ManageContactInformation\Web\ViewHelpers\ModuleContactInformationViewHelper; use App\Contact\ManageContactName\Web\ViewHelpers\ModuleContactNameViewHelper; +use App\Contact\ManageDocuments\Web\ViewHelpers\ModuleDocumentsViewHelper; use App\Contact\ManageGoals\Web\ViewHelpers\ModuleGoalsViewHelper; use App\Contact\ManageGroups\Web\ViewHelpers\GroupsViewHelper; use App\Contact\ManageGroups\Web\ViewHelpers\ModuleGroupsViewHelper; @@ -237,6 +238,10 @@ public static function modules(TemplatePage $page, Contact $contact, User $user) $data = ModuleContactInformationViewHelper::data($contact, $user); } + if ($module->type == Module::TYPE_DOCUMENTS) { + $data = ModuleDocumentsViewHelper::data($contact); + } + $modulesCollection->push([ 'id' => $module->id, 'type' => $module->type, diff --git a/domains/Contact/ManageDocuments/Events/FileDeleted.php b/domains/Contact/ManageDocuments/Events/FileDeleted.php new file mode 100644 index 00000000000..d66794097e2 --- /dev/null +++ b/domains/Contact/ManageDocuments/Events/FileDeleted.php @@ -0,0 +1,25 @@ +file = $file; + } +} diff --git a/domains/Contact/ManageDocuments/Listeners/DeleteFileInStorage.php b/domains/Contact/ManageDocuments/Listeners/DeleteFileInStorage.php new file mode 100644 index 00000000000..ae504439329 --- /dev/null +++ b/domains/Contact/ManageDocuments/Listeners/DeleteFileInStorage.php @@ -0,0 +1,84 @@ +file = $event->file; + $this->checkAPIKeyPresence(); + $this->getFileFromUploadcare(); + $this->deleteFile(); + } + + private function checkAPIKeyPresence(): void + { + if (is_null(config('services.uploadcare.private_key'))) { + throw new EnvVariablesNotSetException(); + } + + if (is_null(config('services.uploadcare.public_key'))) { + throw new EnvVariablesNotSetException(); + } + } + + private function getFileFromUploadcare(): void + { + $configuration = Configuration::create(config('services.uploadcare.public_key'), config('services.uploadcare.private_key')); + $this->api = new Api($configuration); + + try { + $this->fileInUploadcare = $this->api->file()->fileInfo($this->file->uuid); + } catch (HttpException $e) { + throw new BadRequestHttpException($e->getMessage()); + } + } + + private function deleteFile(): void + { + $this->api->file()->deleteFile($this->fileInUploadcare); + } +} diff --git a/domains/Contact/ManageDocuments/Services/DestroyDocument.php b/domains/Contact/ManageDocuments/Services/DestroyDocument.php new file mode 100644 index 00000000000..812b36de5f7 --- /dev/null +++ b/domains/Contact/ManageDocuments/Services/DestroyDocument.php @@ -0,0 +1,74 @@ + 'required|integer|exists:accounts,id', + 'vault_id' => 'required|integer|exists:vaults,id', + 'author_id' => 'required|integer|exists:users,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'file_id' => 'required|integer|exists:files,id', + ]; + } + + /** + * Get the permissions that apply to the user calling the service. + * + * @return array + */ + public function permissions(): array + { + return [ + 'author_must_belong_to_account', + 'vault_must_belong_to_account', + 'contact_must_belong_to_vault', + 'author_must_be_vault_editor', + ]; + } + + /** + * Destroy a file of the document type. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->data = $data; + $this->validate(); + + $this->file->delete(); + + $this->updateLastEditedDate(); + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->file = File::where('contact_id', $this->contact->id) + ->where('type', File::TYPE_DOCUMENT) + ->findOrFail($this->data['file_id']); + } + + private function updateLastEditedDate(): void + { + $this->contact->last_updated_at = Carbon::now(); + $this->contact->save(); + } +} diff --git a/domains/Contact/ManageDocuments/Services/UploadFile.php b/domains/Contact/ManageDocuments/Services/UploadFile.php new file mode 100644 index 00000000000..c02887961ac --- /dev/null +++ b/domains/Contact/ManageDocuments/Services/UploadFile.php @@ -0,0 +1,105 @@ + 'required|integer|exists:accounts,id', + 'vault_id' => 'required|integer|exists:vaults,id', + 'author_id' => 'required|integer|exists:users,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'uuid' => 'required|string', + 'name' => 'required|string', + 'original_url' => 'required|string', + 'cdn_url' => 'required|string', + 'mime_type' => 'required|string', + 'size' => 'required|integer', + 'type' => 'required|string', + ]; + } + + /** + * Get the permissions that apply to the user calling the service. + * + * @return array + */ + public function permissions(): array + { + return [ + 'author_must_belong_to_account', + 'vault_must_belong_to_account', + 'author_must_be_vault_editor', + 'contact_must_belong_to_vault', + ]; + } + + /** + * Upload a file. + * + * This doesn't really upload a file though. Upload is handled by Uploadcare. + * However, we abstract uploads by the File object. This service here takes + * the payload that Uploadcare sends us back, and map it into a File object + * that the clients will consume. + * + * @param array $data + * @return File + */ + public function execute(array $data): File + { + $this->data = $data; + $this->validate(); + $this->save(); + $this->updateLastEditedDate(); + + return $this->file; + } + + private function validate(): void + { + if (is_null(config('services.uploadcare.private_key'))) { + throw new EnvVariablesNotSetException(); + } + + if (is_null(config('services.uploadcare.public_key'))) { + throw new EnvVariablesNotSetException(); + } + + $this->validateRules($this->data); + } + + private function save(): void + { + $this->file = File::create([ + 'contact_id' => $this->data['contact_id'], + 'uuid' => $this->data['uuid'], + 'name' => $this->data['name'], + 'original_url' => $this->data['original_url'], + 'cdn_url' => $this->data['cdn_url'], + 'mime_type' => $this->data['mime_type'], + 'size' => $this->data['size'], + 'type' => $this->data['type'], + ]); + } + + private function updateLastEditedDate(): void + { + $this->contact->last_updated_at = Carbon::now(); + $this->contact->save(); + } +} diff --git a/domains/Contact/ManageDocuments/Web/Controllers/ContactModuleDocumentController.php b/domains/Contact/ManageDocuments/Web/Controllers/ContactModuleDocumentController.php new file mode 100644 index 00000000000..0c39acb1aa3 --- /dev/null +++ b/domains/Contact/ManageDocuments/Web/Controllers/ContactModuleDocumentController.php @@ -0,0 +1,57 @@ + Auth::user()->account_id, + 'author_id' => Auth::user()->id, + 'vault_id' => $vaultId, + 'contact_id' => $contactId, + 'uuid' => $request->input('uuid'), + 'name' => $request->input('name'), + 'original_url' => $request->input('original_url'), + 'cdn_url' => $request->input('cdn_url'), + 'mime_type' => $request->input('mime_type'), + 'size' => $request->input('size'), + 'type' => File::TYPE_DOCUMENT, + ]; + + $file = (new UploadFile())->execute($data); + + $contact = Contact::findOrFail($contactId); + + return response()->json([ + 'data' => ModuleDocumentsViewHelper::dto($file, $contact), + ], 201); + } + + public function destroy(Request $request, int $vaultId, int $contactId, int $fileId) + { + $data = [ + 'account_id' => Auth::user()->account_id, + 'author_id' => Auth::user()->id, + 'vault_id' => $vaultId, + 'contact_id' => $contactId, + 'file_id' => $fileId, + ]; + + (new DestroyDocument())->execute($data); + + return response()->json([ + 'data' => true, + ], 200); + } +} diff --git a/domains/Contact/ManageDocuments/Web/ViewHelpers/ModuleDocumentsViewHelper.php b/domains/Contact/ManageDocuments/Web/ViewHelpers/ModuleDocumentsViewHelper.php new file mode 100644 index 00000000000..b3325ad6a3e --- /dev/null +++ b/domains/Contact/ManageDocuments/Web/ViewHelpers/ModuleDocumentsViewHelper.php @@ -0,0 +1,51 @@ +files() + ->where('type', File::TYPE_DOCUMENT) + ->get() + ->map(function (File $file) use ($contact) { + return self::dto($file, $contact); + }); + + return [ + 'documents' => $documentsCollection, + 'uploadcarePublicKey' => config('services.uploadcare.public_key'), + 'canUploadFile' => StorageHelper::canUploadFile($contact->vault->account), + 'url' => [ + 'store' => route('contact.document.store', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + ]), + ], + ]; + } + + public static function dto(File $file, Contact $contact): array + { + return [ + 'id' => $file->id, + 'download_url' => $file->cdn_url, + 'name' => $file->name, + 'mime_type' => $file->mime_type, + 'size' => FileHelper::formatFileSize($file->size), + 'url' => [ + 'destroy' => route('contact.document.destroy', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + 'document' => $file->id, + ]), + ], + ]; + } +} diff --git a/domains/Settings/CancelAccount/Services/CancelAccount.php b/domains/Settings/CancelAccount/Services/CancelAccount.php index d8b5584909b..e9832e99497 100644 --- a/domains/Settings/CancelAccount/Services/CancelAccount.php +++ b/domains/Settings/CancelAccount/Services/CancelAccount.php @@ -4,11 +4,15 @@ use App\Interfaces\ServiceInterface; use App\Models\Account; +use App\Models\Contact; +use App\Models\File; use App\Models\User; use App\Services\BaseService; class CancelAccount extends BaseService implements ServiceInterface { + private Account $account; + /** * Get the validation rules that apply to the service. * @@ -44,7 +48,21 @@ public function execute(array $data): void { $this->validateRules($data); - $account = Account::findOrFail($data['account_id']); - $account->delete(); + $this->account = Account::findOrFail($data['account_id']); + $this->destroyAllFiles(); + + $this->account->delete(); + } + + private function destroyAllFiles(): void + { + $vaultIds = $this->account->vaults()->select('id')->get()->pluck('id')->toArray(); + $contactIds = Contact::whereIn('vault_id', $vaultIds)->select('id')->get()->pluck('id')->toArray(); + + File::whereIn('contact_id', $contactIds)->chunk(100, function ($files) { + $files->each(function ($file) { + $file->delete(); + }); + }); } } diff --git a/domains/Settings/CreateAccount/Services/CreateAccount.php b/domains/Settings/CreateAccount/Services/CreateAccount.php index ceafe58da40..dd8c52995ee 100644 --- a/domains/Settings/CreateAccount/Services/CreateAccount.php +++ b/domains/Settings/CreateAccount/Services/CreateAccount.php @@ -42,7 +42,9 @@ public function execute(array $data): User $this->data = $data; $this->validateRules($this->data); - $this->account = Account::create(); + $this->account = Account::create([ + 'storage_limit_in_mb' => config('monica.default_storage_limit_in_mb'), + ]); $this->addFirstUser(); $this->addLogs(); diff --git a/domains/Settings/ManageSettings/Web/ViewHelpers/SettingsIndexViewHelper.php b/domains/Settings/ManageSettings/Web/ViewHelpers/SettingsIndexViewHelper.php index 6cc3fb8f040..3cd19a1d922 100644 --- a/domains/Settings/ManageSettings/Web/ViewHelpers/SettingsIndexViewHelper.php +++ b/domains/Settings/ManageSettings/Web/ViewHelpers/SettingsIndexViewHelper.php @@ -23,6 +23,9 @@ public static function data(User $user): array 'personalize' => [ 'index' => route('settings.personalize.index'), ], + 'storage' => [ + 'index' => route('settings.storage.index'), + ], 'cancel' => [ 'index' => route('settings.cancel.index'), ], diff --git a/domains/Settings/ManageStorage/Web/Controllers/AccountStorageController.php b/domains/Settings/ManageStorage/Web/Controllers/AccountStorageController.php new file mode 100644 index 00000000000..a37c4ac0864 --- /dev/null +++ b/domains/Settings/ManageStorage/Web/Controllers/AccountStorageController.php @@ -0,0 +1,20 @@ + VaultIndexViewHelper::layoutData(), + 'data' => StorageIndexViewHelper::data(Auth::user()->account), + ]); + } +} diff --git a/domains/Settings/ManageStorage/Web/ViewHelpers/StorageIndexViewHelper.php b/domains/Settings/ManageStorage/Web/ViewHelpers/StorageIndexViewHelper.php new file mode 100644 index 00000000000..78f161e4ece --- /dev/null +++ b/domains/Settings/ManageStorage/Web/ViewHelpers/StorageIndexViewHelper.php @@ -0,0 +1,73 @@ +vaults()->select('id')->get()->pluck('id')->toArray(); + $contactIds = Contact::whereIn('vault_id', $vaultIds)->select('id')->get()->pluck('id')->toArray(); + + $totalSizeDocumentInBytes = File::whereIn('contact_id', $contactIds) + ->where('type', File::TYPE_DOCUMENT) + ->sum('size'); + + $totalSizeAvatarInBytes = File::whereIn('contact_id', $contactIds) + ->where('type', File::TYPE_AVATAR) + ->sum('size'); + + $totalSizePhotosInBytes = File::whereIn('contact_id', $contactIds) + ->where('type', File::TYPE_PHOTO) + ->sum('size'); + + $totalSizeInBytes = $totalSizeDocumentInBytes + $totalSizeAvatarInBytes + $totalSizePhotosInBytes; + + $totalNumberOfPhotos = File::whereIn('contact_id', $contactIds) + ->where('type', File::TYPE_PHOTO) + ->count(); + + $totalNumberOfDocuments = File::whereIn('contact_id', $contactIds) + ->where('type', File::TYPE_DOCUMENT) + ->count(); + + $totalNumberOfAvatars = File::whereIn('contact_id', $contactIds) + ->where('type', File::TYPE_AVATAR) + ->count(); + + $accountLimit = $account->storage_limit_in_mb * 1024 * 1024; + + return [ + 'statistics' => [ + 'total' => FileHelper::formatFileSize($totalSizeInBytes), + 'total_percent' => $accountLimit > 0 ? round($totalSizeInBytes * 100 / $accountLimit, 0) : 0, + 'photo' => [ + 'total' => $totalNumberOfPhotos, + 'total_percent' => $totalSizeInBytes > 0 ? round($totalSizePhotosInBytes * 100 / $totalSizeInBytes, 0) : 0, + 'size' => FileHelper::formatFileSize($totalSizePhotosInBytes), + ], + 'document' => [ + 'total' => $totalNumberOfDocuments, + 'total_percent' => $totalSizeDocumentInBytes > 0 ? round($totalSizeDocumentInBytes * 100 / $totalSizeInBytes, 0) : 0, + 'size' => FileHelper::formatFileSize($totalSizeDocumentInBytes), + ], + 'avatar' => [ + 'total' => $totalNumberOfAvatars, + 'total_percent' => $totalSizeAvatarInBytes > 0 ? round($totalSizeAvatarInBytes * 100 / $totalSizeInBytes, 0) : 0, + 'size' => FileHelper::formatFileSize($totalSizeAvatarInBytes), + ], + ], + 'account_limit' => FileHelper::formatFileSize($accountLimit), + 'url' => [ + 'settings' => [ + 'index' => route('settings.index'), + ], + ], + ]; + } +} diff --git a/domains/Vault/ManageFiles/Web/Controllers/VaultFileController.php b/domains/Vault/ManageFiles/Web/Controllers/VaultFileController.php new file mode 100644 index 00000000000..a381ae0bcd9 --- /dev/null +++ b/domains/Vault/ManageFiles/Web/Controllers/VaultFileController.php @@ -0,0 +1,96 @@ +id)->select('id')->get()->pluck('id')->toArray(); + + $files = File::whereIn('contact_id', $contactIds) + ->with('contact') + ->orderBy('created_at', 'desc') + ->paginate(25); + + return Inertia::render('Vault/Files/Index', [ + 'layoutData' => VaultIndexViewHelper::layoutData($vault), + 'data' => VaultFileIndexViewHelper::data($files, Auth::user(), $vault), + 'paginator' => PaginatorHelper::getData($files), + 'tab' => 'index', + ]); + } + + public function photos(Request $request, int $vaultId) + { + $vault = Vault::findOrFail($vaultId); + + $contactIds = Contact::where('vault_id', $vault->id)->select('id')->get()->pluck('id')->toArray(); + + $files = File::whereIn('contact_id', $contactIds) + ->where('type', File::TYPE_PHOTO) + ->with('contact') + ->orderBy('created_at', 'desc') + ->paginate(25); + + return Inertia::render('Vault/Files/Index', [ + 'layoutData' => VaultIndexViewHelper::layoutData($vault), + 'data' => VaultFileIndexViewHelper::data($files, Auth::user(), $vault), + 'paginator' => PaginatorHelper::getData($files), + 'tab' => 'photos', + ]); + } + + public function documents(Request $request, int $vaultId) + { + $vault = Vault::findOrFail($vaultId); + + $contactIds = Contact::where('vault_id', $vault->id)->select('id')->get()->pluck('id')->toArray(); + + $files = File::whereIn('contact_id', $contactIds) + ->where('type', File::TYPE_DOCUMENT) + ->with('contact') + ->orderBy('created_at', 'desc') + ->paginate(25); + + return Inertia::render('Vault/Files/Index', [ + 'layoutData' => VaultIndexViewHelper::layoutData($vault), + 'data' => VaultFileIndexViewHelper::data($files, Auth::user(), $vault), + 'paginator' => PaginatorHelper::getData($files), + 'tab' => 'documents', + ]); + } + + public function avatars(Request $request, int $vaultId) + { + $vault = Vault::findOrFail($vaultId); + + $contactIds = Contact::where('vault_id', $vault->id)->select('id')->get()->pluck('id')->toArray(); + + $files = File::whereIn('contact_id', $contactIds) + ->where('type', File::TYPE_AVATAR) + ->with('contact') + ->orderBy('created_at', 'desc') + ->paginate(25); + + return Inertia::render('Vault/Files/Index', [ + 'layoutData' => VaultIndexViewHelper::layoutData($vault), + 'data' => VaultFileIndexViewHelper::data($files, Auth::user(), $vault), + 'paginator' => PaginatorHelper::getData($files), + 'tab' => 'avatars', + ]); + } +} diff --git a/domains/Vault/ManageFiles/Web/ViewHelpers/VaultFileIndexViewHelper.php b/domains/Vault/ManageFiles/Web/ViewHelpers/VaultFileIndexViewHelper.php new file mode 100644 index 00000000000..74f1bb11cd5 --- /dev/null +++ b/domains/Vault/ManageFiles/Web/ViewHelpers/VaultFileIndexViewHelper.php @@ -0,0 +1,93 @@ +contact; + + $filesCollection->push([ + 'id' => $file->id, + 'download_url' => $file->cdn_url, + 'name' => $file->name, + 'mime_type' => $file->mime_type, + 'size' => FileHelper::formatFileSize($file->size), + 'created_at' => DateHelper::format($file->created_at, $user), + 'contact' => [ + 'id' => $contact->id, + 'name' => $contact->name, + 'avatar' => $contact->avatar, + 'url' => [ + 'show' => route('contact.show', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + ]), + ], + ], + 'url' => [ + 'destroy' => route('contact.document.destroy', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + 'document' => $file->id, + ]), + ], + ]); + } + + return [ + 'files' => $filesCollection, + 'statistics' => self::statistics($vault), + ]; + } + + public static function statistics(Vault $vault): array + { + $contactIds = Contact::where('vault_id', $vault->id)->select('id')->get()->pluck('id')->toArray(); + + $totalNumberOfPhotos = File::whereIn('contact_id', $contactIds) + ->where('type', File::TYPE_PHOTO) + ->count(); + + $totalNumberOfDocuments = File::whereIn('contact_id', $contactIds) + ->where('type', File::TYPE_DOCUMENT) + ->count(); + + $totalNumberOfAvatars = File::whereIn('contact_id', $contactIds) + ->where('type', File::TYPE_AVATAR) + ->count(); + + return [ + 'statistics' => [ + 'all' => $totalNumberOfPhotos + $totalNumberOfDocuments + $totalNumberOfAvatars, + 'photos' => $totalNumberOfPhotos, + 'documents' => $totalNumberOfDocuments, + 'avatars' => $totalNumberOfAvatars, + ], + 'url' => [ + 'index' => route('vault.files.index', [ + 'vault' => $vault->id, + ]), + 'photos' => route('vault.files.photos', [ + 'vault' => $vault->id, + ]), + 'documents' => route('vault.files.documents', [ + 'vault' => $vault->id, + ]), + 'avatars' => route('vault.files.avatars', [ + 'vault' => $vault->id, + ]), + ], + ]; + } +} diff --git a/domains/Vault/ManageVault/Services/DestroyVault.php b/domains/Vault/ManageVault/Services/DestroyVault.php index 22a84495c8e..88496bf7af9 100644 --- a/domains/Vault/ManageVault/Services/DestroyVault.php +++ b/domains/Vault/ManageVault/Services/DestroyVault.php @@ -4,6 +4,8 @@ use App\Interfaces\ServiceInterface; use App\Jobs\CreateAuditLog; +use App\Models\Contact; +use App\Models\File; use App\Services\BaseService; class DestroyVault extends BaseService implements ServiceInterface @@ -45,6 +47,18 @@ public function execute(array $data): void { $this->validateRules($data); + $contactIds = Contact::where('vault_id', $this->vault->id) + ->select('id') + ->get() + ->pluck('id') + ->toArray(); + + File::whereIn('contact_id', $contactIds)->chunk(100, function ($files) { + $files->each(function ($file) { + $file->delete(); + }); + }); + $this->vault->delete(); CreateAuditLog::dispatch([ diff --git a/domains/Vault/ManageVault/Web/ViewHelpers/VaultIndexViewHelper.php b/domains/Vault/ManageVault/Web/ViewHelpers/VaultIndexViewHelper.php index db037f42753..39d65c86f2d 100644 --- a/domains/Vault/ManageVault/Web/ViewHelpers/VaultIndexViewHelper.php +++ b/domains/Vault/ManageVault/Web/ViewHelpers/VaultIndexViewHelper.php @@ -48,6 +48,9 @@ public static function layoutData(Vault $vault = null): array 'contacts' => route('contact.index', [ 'vault' => $vault->id, ]), + 'files' => route('vault.files.index', [ + 'vault' => $vault->id, + ]), 'settings' => route('vault.settings.index', [ 'vault' => $vault->id, ]), diff --git a/lang/en/app.php b/lang/en/app.php index 59e4341eaca..6a79245b596 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -7,8 +7,9 @@ 'layout_menu_dashboard' => 'Dashboard', 'layout_menu_reports' => 'Reports', 'layout_menu_contacts' => 'Contacts', - 'layout_menu_gift_center' => 'Gift center', - 'layout_menu_loans' => 'Loans & debts center', + 'layout_menu_gift_center' => 'Gifts', + 'layout_menu_loans' => 'Loans & debts', + 'layout_menu_files' => 'Files', 'layout_menu_vault_settings' => 'Vault settings', 'layout_footer_monica' => 'Monica, since 2017.', 'layout_footer_version' => 'Current version: :version', @@ -33,6 +34,7 @@ 'breadcrumb_settings_personalize_templates' => 'Templates', 'breadcrumb_settings_personalize_relationship_types' => 'Relationship types', 'breadcrumb_settings_personalize_contact_information_types' => 'Contact information types', + 'breadcrumb_settings_storage' => 'Storage', 'notification_flash_changes_saved' => 'Changes saved', @@ -60,6 +62,7 @@ 'next' => 'Next', 'view_all' => 'View all', 'view_map' => 'View on map', + 'download' => 'Download', 'error_title' => '👇 Oops. An error occured.', @@ -95,6 +98,7 @@ 'module_addresses' => 'Addresses', 'module_groups' => 'Groups', 'module_contact_information' => 'Contact information', + 'module_documents' => 'Documents', 'module_option_default_number_of_items_to_display' => 'Default number of items to display', diff --git a/lang/en/contact.php b/lang/en/contact.php index 350ffa10c21..6f86ce29984 100644 --- a/lang/en/contact.php +++ b/lang/en/contact.php @@ -76,4 +76,17 @@ 'contact_information_edit_success' => 'The contact information has been edited', 'contact_information_delete_confirm' => 'Are you sure? This will delete the contact information permanently.', 'contact_information_delete_success' => 'The contact information has been deleted', + + /*************************************************************** + * MODULE: DOCUMENTS + **************************************************************/ + + 'documents_title' => 'Documents', + 'documents_cta' => 'Add a document', + 'documents_blank' => 'There are no documents yet.', + 'documents_not_enough_storage' => 'You don’t have enough space left in your account.', + 'documents_key_missing' => 'The keys to manage uploads have not been set in this Monica instance.', + 'documents_new_success' => 'The document has been added', + 'documents_delete_confirm' => 'Are you sure? This will delete the document permanently.', + 'documents_delete_success' => 'The document has been deleted', ]; diff --git a/lang/en/settings.php b/lang/en/settings.php index c0780455c0f..07f64eece7d 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -6,6 +6,7 @@ 'notification_channels' => 'Notification channels', 'account_settings' => 'Account settings', 'manage_users' => 'Manage users', + 'manage_storage' => 'Manage storage', 'personalize_your_contacts_data' => 'Personalize your contacts data', 'cancel_your_account' => 'Cancel your account', @@ -205,4 +206,15 @@ 'personalize_contact_information_types_edit_success' => 'The contact information type has been updated', 'personalize_contact_information_types_delete_success' => 'The contact information type has been deleted', 'personalize_contact_information_types_blank' => 'Are you sure? This will remove the contact information types from all contacts, but won’t delete the contacts themselves.', + + /*************************************************************** + * STORAGE + **************************************************************/ + + 'storage_title' => 'Storage', + 'storage_account_limit' => 'Your account limit', + 'storage_account_current_usage' => 'Your account current usage', + 'storage_type_document' => 'Documents', + 'storage_type_avatar' => 'Avatars', + 'storage_type_photo' => 'Photos', ]; diff --git a/lang/en/vault.php b/lang/en/vault.php index 8f16f763a08..ce28d6d2e18 100644 --- a/lang/en/vault.php +++ b/lang/en/vault.php @@ -59,17 +59,29 @@ * VAULT DASHBOARD **************************************************************/ - 'dashboard_last_updated_contacts_title' => 'Last updated', - 'dashboard_reminders_title' => 'Reminders for the next 30 days', - 'dashboard_reminders_blank' => 'No planned reminders.', + 'dashboard_last_updated_contacts_title' => 'Last updated', + 'dashboard_reminders_title' => 'Reminders for the next 30 days', + 'dashboard_reminders_blank' => 'No planned reminders.', 'dashboard_favorites_title' => 'Favorites', /*************************************************************** * VAULT DASHBOARD REMINDERS **************************************************************/ - 'reminders_title' => 'Reminders planned in the next 12 months', - 'reminders_blank' => 'No entries for this month', + 'reminders_title' => 'Reminders planned in the next 12 months', + 'reminders_blank' => 'No entries for this month', + + /*************************************************************** + * VAULT FILES + **************************************************************/ + + 'files_filter_title' => 'All the files', + 'files_filter_all' => 'All files', + 'files_filter_or' => 'Or filter by type', + 'files_filter_documents' => 'Documents', + 'files_filter_photos' => 'Photos', + 'files_filter_avatars' => 'Avatars', + 'files_filter_blank' => 'There are no files yet.', /*************************************************************** * VAULT SETTINGS diff --git a/lang/fr/app.php b/lang/fr/app.php index d77f881d523..44064622f7d 100644 --- a/lang/fr/app.php +++ b/lang/fr/app.php @@ -7,8 +7,9 @@ 'layout_menu_dashboard' => 'Tableau de bord', 'layout_menu_reports' => 'Rapports', 'layout_menu_contacts' => 'Contacts', - 'layout_menu_gift_center' => 'Centre de cadeaux', - 'layout_menu_loans' => 'Centre des prêts et dettes', + 'layout_menu_gift_center' => 'Cadeaux', + 'layout_menu_loans' => 'Prêts et dettes', + 'layout_menu_files' => 'Fichiers', 'layout_menu_vault_settings' => 'Paramètres de la voûte', 'layout_footer_monica' => 'Monica, depuis 2017.', 'layout_footer_version' => 'Version courante: :version', @@ -33,6 +34,7 @@ 'breadcrumb_settings_personalize_templates' => 'Modèles', 'breadcrumb_settings_personalize_relationship_types' => 'Types de relation', 'breadcrumb_settings_personalize_contact_information_types' => 'Types d’information de contact', + 'breadcrumb_settings_storage' => 'Stockage', 'notification_flash_changes_saved' => 'Changements effectués', @@ -60,6 +62,7 @@ 'next' => 'Suivant', 'view_all' => 'Tout voir', 'view_map' => 'Voir sur la carte', + 'download' => 'Télécharger', 'error_title' => '👇 Oops. Une erreur est survenue.', @@ -95,6 +98,7 @@ 'module_addresses' => 'Adresses', 'module_groups' => 'Groupes', 'module_contact_information' => 'Information de contact', + 'module_documents' => 'Documents', 'module_option_default_number_of_items_to_display' => 'Nombre par défaut d’éléments à afficher', diff --git a/lang/fr/contact.php b/lang/fr/contact.php index 8cb30f76117..34f167b802f 100644 --- a/lang/fr/contact.php +++ b/lang/fr/contact.php @@ -76,4 +76,16 @@ 'contact_information_edit_success' => 'L’information de contact a été mise à jour', 'contact_information_delete_confirm' => 'Êtes vous sûr ? Cela va supprimer l’information de contact de façon permanente.', 'contact_information_delete_success' => 'L’information de contact a été supprimée', + + /*************************************************************** + * MODULE: DOCUMENTS + **************************************************************/ + + 'documents_title' => 'Documents', + 'documents_cta' => 'Ajouter un document', + 'documents_blank' => 'Il n’y a pas encore d’information de documents.', + 'documents_key_missing' => 'Les clés pour gérer les uploads n’ont pas été définies sur cette instance de Monica.', + 'documents_new_success' => 'Le document a été ajouté', + 'documents_delete_confirm' => 'Êtes vous sûr ? Cela va supprimer le document de contact de façon permanente.', + 'documents_delete_success' => 'Le document a été supprimé', ]; diff --git a/lang/fr/settings.php b/lang/fr/settings.php index 0f84d6bd6ca..fd62cc23ce7 100644 --- a/lang/fr/settings.php +++ b/lang/fr/settings.php @@ -6,6 +6,7 @@ 'notification_channels' => 'Canaux de notifications', 'account_settings' => 'Paramètres du compte', 'manage_users' => 'Gestion des utilisateurs', + 'manage_storage' => 'Gestion du stockage', 'personalize_your_contacts_data' => 'Personnalisation des données de contacts', 'cancel_your_account' => 'Annulation du compte', @@ -205,4 +206,15 @@ 'personalize_contact_information_types_edit_success' => 'Le type d’information de contact a été mis à jour', 'personalize_contact_information_types_delete_success' => 'Le type d’information de contact a été supprimé', 'personalize_contact_information_types_blank' => 'Êtes-vous sûr ? Cela va supprimer toutes les informations de contact de ce type pour tous les contacts qui l’utilisaient, sans supprimer les contacts eux mêmes.', + + /*************************************************************** + * STORAGE + **************************************************************/ + + 'storage_title' => 'Stockage', + 'storage_account_limit' => 'La limite de votre compte', + 'storage_account_current_usage' => 'Votre utilisation courante', + 'storage_type_document' => 'Documents', + 'storage_type_avatar' => 'Avatars', + 'storage_type_photo' => 'Photos', ]; diff --git a/lang/fr/vault.php b/lang/fr/vault.php index d7391af5e7b..1162003b131 100644 --- a/lang/fr/vault.php +++ b/lang/fr/vault.php @@ -71,6 +71,18 @@ 'reminders_title' => 'Rappels planifiés dans les 12 prochains mois', 'reminders_blank' => 'Pas de rappels pour ce mois', + /*************************************************************** + * VAULT FILES + **************************************************************/ + + 'files_filter_title' => 'Tous les fichiers', + 'files_filter_all' => 'Tous les fichiers', + 'files_filter_or' => 'Ou filtrer par type', + 'files_filter_documents' => 'Documents', + 'files_filter_photos' => 'Photos', + 'files_filter_avatars' => 'Avatars', + 'files_filter_blank' => 'Il n’y a pas encore de fichiers.', + /*************************************************************** * VAULT SETTINGS **************************************************************/ diff --git a/package.json b/package.json index aff6f11c7db..22de0174408 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "sass-loader": "^13.0.0", "tailwindcss": "^3.0.24", "tiny-emitter": "^2.1.0", + "uploadcare-vue": "^1.0.0", "v-calendar": "^3.0.0-alpha.8", "vue": "^3.2.36", "vue-loader": "^17.0.0", diff --git a/phpstan.neon b/phpstan.neon index 3446ebdb951..68167f28b97 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,6 +9,7 @@ parameters: - tests/ApiTestCase.php - tests/TestCase.php - app/Providers/RouteServiceProvider.php + - app/Helpers/StorageHelper.php inferPrivatePropertyTypeFromConstructor: true checkMissingIterableValueType: false reportUnmatchedIgnoredErrors: false diff --git a/resources/css/app.css b/resources/css/app.css index 1eca2035803..d187186a3e2 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -18,3 +18,7 @@ body { padding-left: 4px; content: '•'; } + +.uploadcare--button_primary { + background: #3891ff; +} diff --git a/resources/js/Pages/Settings/Index.vue b/resources/js/Pages/Settings/Index.vue index f5b69d2005e..575d837fc3b 100644 --- a/resources/js/Pages/Settings/Index.vue +++ b/resources/js/Pages/Settings/Index.vue @@ -51,6 +51,12 @@ $t('settings.personalize_your_contacts_data') }} +
  • + 📸 + {{ + $t('settings.manage_storage') + }} +
  • đź’© {{ diff --git a/resources/js/Pages/Settings/Storage/Index.vue b/resources/js/Pages/Settings/Storage/Index.vue new file mode 100644 index 00000000000..7e8626b211d --- /dev/null +++ b/resources/js/Pages/Settings/Storage/Index.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/resources/js/Pages/Vault/Contact/Show.vue b/resources/js/Pages/Vault/Contact/Show.vue index 7872c427cb8..de4a73c5d99 100644 --- a/resources/js/Pages/Vault/Contact/Show.vue +++ b/resources/js/Pages/Vault/Contact/Show.vue @@ -154,6 +154,8 @@ + + @@ -184,6 +186,7 @@ import Goals from '@/Shared/Modules/Goals'; import Addresses from '@/Shared/Modules/Addresses'; import Groups from '@/Shared/Modules/Groups'; import ContactInformation from '@/Shared/Modules/ContactInformation'; +import Documents from '@/Shared/Modules/Documents'; export default { components: { @@ -207,6 +210,7 @@ export default { Addresses, Groups, ContactInformation, + Documents, }, props: { @@ -241,6 +245,7 @@ export default { addresses: [], groups: [], contactInformation: [], + documents: [], }; }, @@ -338,6 +343,10 @@ export default { this.contactInformation = this.data.modules[this.data.modules.findIndex((x) => x.type == 'contact_information')].data; } + + if (this.data.modules.findIndex((x) => x.type == 'documents') > -1) { + this.documents = this.data.modules[this.data.modules.findIndex((x) => x.type == 'documents')].data; + } } }, diff --git a/resources/js/Pages/Vault/Files/Index.vue b/resources/js/Pages/Vault/Files/Index.vue new file mode 100644 index 00000000000..98d9da98615 --- /dev/null +++ b/resources/js/Pages/Vault/Files/Index.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/resources/js/Shared/Layout.vue b/resources/js/Shared/Layout.vue index ea106acfb36..15cb8c09634 100644 --- a/resources/js/Shared/Layout.vue +++ b/resources/js/Shared/Layout.vue @@ -152,6 +152,13 @@ main { {{ $t('app.layout_menu_loans') }} + + {{ $t('app.layout_menu_files') }} + + +.icon-sidebar { + color: #737e8d; + top: -2px; +} + +.item-list { + &:hover:first-child { + border-top-left-radius: 8px; + border-top-right-radius: 8px; + } + + &:last-child { + border-bottom: 0; + } + + &:hover:last-child { + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + } +} + +select { + padding-left: 8px; + padding-right: 20px; + background-position: right 3px center; +} + + + + + diff --git a/routes/web.php b/routes/web.php index 5433d623e33..20943b33fb2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -11,6 +11,7 @@ use App\Contact\ManageContactAddresses\Web\Controllers\ContactModuleAddressController; use App\Contact\ManageContactImportantDates\Web\Controllers\ContactImportantDatesController; use App\Contact\ManageContactInformation\Web\Controllers\ContactInformationController; +use App\Contact\ManageDocuments\Web\Controllers\ContactModuleDocumentController; use App\Contact\ManageGoals\Web\Controllers\ContactModuleGoalController; use App\Contact\ManageGoals\Web\Controllers\ContactModuleStreakController; use App\Contact\ManageGroups\Web\Controllers\ContactModuleGroupController; @@ -60,6 +61,7 @@ use App\Settings\ManageRelationshipTypes\Web\Controllers\PersonalizeRelationshipController; use App\Settings\ManageRelationshipTypes\Web\Controllers\PersonalizeRelationshipTypeController; use App\Settings\ManageSettings\Web\Controllers\SettingsController; +use App\Settings\ManageStorage\Web\Controllers\AccountStorageController; use App\Settings\ManageTemplates\Web\Controllers\PersonalizeTemplatePageModulesController; use App\Settings\ManageTemplates\Web\Controllers\PersonalizeTemplatePageModulesPositionController; use App\Settings\ManageTemplates\Web\Controllers\PersonalizeTemplatePagePositionController; @@ -73,6 +75,7 @@ use App\Settings\ManageUserPreferences\Web\Controllers\PreferencesNumberFormatController; use App\Settings\ManageUserPreferences\Web\Controllers\PreferencesTimezoneController; use App\Settings\ManageUsers\Web\Controllers\UserController; +use App\Vault\ManageFiles\Web\Controllers\VaultFileController; use App\Vault\ManageVault\Web\Controllers\VaultController; use App\Vault\ManageVault\Web\Controllers\VaultReminderController; use App\Vault\ManageVaultSettings\Web\Controllers\VaultSettingsContactImportantDateTypeController; @@ -115,6 +118,8 @@ Route::middleware(['vault'])->prefix('{vault}')->group(function () { Route::get('', [VaultController::class, 'show'])->name('vault.show'); + + // reminders Route::get('reminders', [VaultReminderController::class, 'index'])->name('vault.reminder.index'); // vault contacts @@ -198,6 +203,10 @@ Route::put('pets/{pet}', [ContactModulePetController::class, 'update'])->name('contact.pet.update'); Route::delete('pets/{pet}', [ContactModulePetController::class, 'destroy'])->name('contact.pet.destroy'); + // documents + Route::post('documents', [ContactModuleDocumentController::class, 'store'])->name('contact.document.store'); + Route::delete('documents/{document}', [ContactModuleDocumentController::class, 'destroy'])->name('contact.document.destroy'); + // tasks Route::get('tasks/completed', [ContactModuleTaskController::class, 'index'])->name('contact.task.index'); Route::post('tasks', [ContactModuleTaskController::class, 'store'])->name('contact.task.store'); @@ -217,6 +226,14 @@ }); }); + // vault files + Route::prefix('files')->name('vault.files.')->group(function () { + Route::get('', [VaultFileController::class, 'index'])->name('index'); + Route::get('photos', [VaultFileController::class, 'photos'])->name('photos'); + Route::get('documents', [VaultFileController::class, 'documents'])->name('documents'); + Route::get('avatars', [VaultFileController::class, 'avatars'])->name('avatars'); + }); + // vault settings Route::middleware(['atLeastVaultManager'])->group(function () { Route::get('settings', [VaultSettingsController::class, 'index'])->name('vault.settings.index'); @@ -430,6 +447,9 @@ Route::delete('currencies', [PersonalizeCurrencyController::class, 'destroy'])->name('currency.destroy'); }); + // storage + Route::get('storage', [AccountStorageController::class, 'index'])->name('storage.index'); + // cancel Route::get('cancel', [CancelAccountController::class, 'index'])->name('cancel.index'); Route::put('cancel', [CancelAccountController::class, 'destroy'])->name('cancel.destroy'); diff --git a/tests/Unit/Domains/Contact/ManageContact/Services/DestroyContactTest.php b/tests/Unit/Domains/Contact/ManageContact/Services/DestroyContactTest.php index d42435b95f7..21d2e9ebe43 100644 --- a/tests/Unit/Domains/Contact/ManageContact/Services/DestroyContactTest.php +++ b/tests/Unit/Domains/Contact/ManageContact/Services/DestroyContactTest.php @@ -8,10 +8,12 @@ use App\Jobs\CreateAuditLog; use App\Models\Account; use App\Models\Contact; +use App\Models\File; use App\Models\User; use App\Models\Vault; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Queue; use Illuminate\Validation\ValidationException; use Tests\TestCase; @@ -27,6 +29,11 @@ public function it_destroys_a_contact(): void $vault = $this->createVault($regis->account); $vault = $this->setPermissionInVault($regis, Vault::PERMISSION_EDIT, $vault); $contact = Contact::factory()->create(['vault_id' => $vault->id]); + + File::factory()->create([ + 'contact_id' => $contact->id, + ]); + $this->executeService($regis, $regis->account, $vault, $contact); } @@ -100,6 +107,7 @@ public function it_fails_if_contact_cant_be_deleted(): void private function executeService(User $author, Account $account, Vault $vault, Contact $contact): void { Queue::fake(); + Event::fake(); $request = [ 'account_id' => $account->id, @@ -114,6 +122,10 @@ private function executeService(User $author, Account $account, Vault $vault, Co 'id' => $contact->id, ]); + $this->assertDatabaseMissing('files', [ + 'contact_id' => $contact->id, + ]); + Queue::assertPushed(CreateAuditLog::class, function ($job) { return $job->auditLog['action_name'] === 'contact_destroyed'; }); diff --git a/tests/Unit/Domains/Contact/ManageDocuments/Services/DestroyDocumentTest.php b/tests/Unit/Domains/Contact/ManageDocuments/Services/DestroyDocumentTest.php new file mode 100644 index 00000000000..b46dc1dae9f --- /dev/null +++ b/tests/Unit/Domains/Contact/ManageDocuments/Services/DestroyDocumentTest.php @@ -0,0 +1,150 @@ +createUser(); + $vault = $this->createVault($regis->account); + $vault = $this->setPermissionInVault($regis, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $file = File::factory()->create([ + 'contact_id' => $contact->id, + 'type' => File::TYPE_DOCUMENT, + ]); + + $this->executeService($regis, $regis->account, $vault, $contact, $file); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'title' => 'Ross', + ]; + + $this->expectException(ValidationException::class); + (new DestroyDocument())->execute($request); + } + + /** @test */ + public function it_fails_if_user_doesnt_belong_to_account(): void + { + $this->expectException(ModelNotFoundException::class); + + $regis = $this->createUser(); + $account = Account::factory()->create(); + $vault = $this->createVault($regis->account); + $vault = $this->setPermissionInVault($regis, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $file = File::factory()->create([ + 'contact_id' => $contact->id, + 'type' => File::TYPE_DOCUMENT, + ]); + + $this->executeService($regis, $account, $vault, $contact, $file); + } + + /** @test */ + public function it_fails_if_contact_doesnt_belong_to_vault(): void + { + $this->expectException(ModelNotFoundException::class); + + $regis = $this->createUser(); + $vault = $this->createVault($regis->account); + $vault = $this->setPermissionInVault($regis, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(); + $file = File::factory()->create([ + 'contact_id' => $contact->id, + 'type' => File::TYPE_DOCUMENT, + ]); + + $this->executeService($regis, $regis->account, $vault, $contact, $file); + } + + /** @test */ + public function it_fails_if_user_doesnt_have_right_permission_in_initial_vault(): void + { + $this->expectException(NotEnoughPermissionException::class); + + $regis = $this->createUser(); + $vault = $this->createVault($regis->account); + $vault = $this->setPermissionInVault($regis, Vault::PERMISSION_VIEW, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $file = File::factory()->create([ + 'contact_id' => $contact->id, + 'type' => File::TYPE_DOCUMENT, + ]); + + $this->executeService($regis, $regis->account, $vault, $contact, $file); + } + + /** @test */ + public function it_fails_if_file_doesnt_belong_to_contact(): void + { + $this->expectException(ModelNotFoundException::class); + + $regis = $this->createUser(); + $vault = $this->createVault($regis->account); + $vault = $this->setPermissionInVault($regis, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $file = File::factory()->create([ + 'type' => File::TYPE_DOCUMENT, + ]); + + $this->executeService($regis, $regis->account, $vault, $contact, $file); + } + + /** @test */ + public function it_fails_if_file_is_not_a_document(): void + { + $this->expectException(ModelNotFoundException::class); + + $regis = $this->createUser(); + $vault = $this->createVault($regis->account); + $vault = $this->setPermissionInVault($regis, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $file = File::factory()->create([ + 'contact_id' => $contact->id, + ]); + + $this->executeService($regis, $regis->account, $vault, $contact, $file); + } + + private function executeService(User $author, Account $account, Vault $vault, Contact $contact, File $file): void + { + Event::fake(); + + $request = [ + 'account_id' => $account->id, + 'vault_id' => $vault->id, + 'author_id' => $author->id, + 'contact_id' => $contact->id, + 'file_id' => $file->id, + ]; + + (new DestroyDocument())->execute($request); + + $this->assertDatabaseMissing('files', [ + 'id' => $file->id, + ]); + } +} diff --git a/tests/Unit/Domains/Contact/ManageDocuments/Services/UploadFileTest.php b/tests/Unit/Domains/Contact/ManageDocuments/Services/UploadFileTest.php new file mode 100644 index 00000000000..70522eb9fff --- /dev/null +++ b/tests/Unit/Domains/Contact/ManageDocuments/Services/UploadFileTest.php @@ -0,0 +1,156 @@ + 'test']); + config(['services.uploadcare.private_key' => 'test']); + + $regis = $this->createUser(); + $vault = $this->createVault($regis->account); + $vault = $this->setPermissionInVault($regis, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + + $this->executeService($regis, $regis->account, $vault, $contact); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + config(['services.uploadcare.public_key' => 'test']); + config(['services.uploadcare.private_key' => 'test']); + + $request = [ + 'title' => 'Ross', + ]; + + $this->expectException(ValidationException::class); + (new UploadFile())->execute($request); + } + + /** @test */ + public function it_throws_an_exception_when_env_keys_are_not_set(): void + { + config(['services.uploadcare.public_key' => null]); + config(['services.uploadcare.public_key' => null]); + + $regis = $this->createUser(); + $vault = $this->createVault($regis->account); + $vault = $this->setPermissionInVault($regis, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + + $this->expectException(EnvVariablesNotSetException::class); + $this->executeService($regis, $regis->account, $vault, $contact); + + config(['services.uploadcare.public_key' => 'test']); + $this->expectException(EnvVariablesNotSetException::class); + $this->executeService($regis, $regis->account, $vault, $contact); + + config(['services.uploadcare.private_key' => 'test']); + $this->executeService($regis, $regis->account, $vault, $contact); + } + + /** @test */ + public function it_fails_if_user_doesnt_belong_to_account(): void + { + config(['services.uploadcare.public_key' => 'test']); + config(['services.uploadcare.private_key' => 'test']); + + $this->expectException(ModelNotFoundException::class); + + $regis = $this->createUser(); + $account = Account::factory()->create(); + $vault = $this->createVault($regis->account); + $vault = $this->setPermissionInVault($regis, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + + $this->executeService($regis, $account, $vault, $contact); + } + + /** @test */ + public function it_fails_if_contact_doesnt_belong_to_vault(): void + { + config(['services.uploadcare.public_key' => 'test']); + config(['services.uploadcare.private_key' => 'test']); + + $this->expectException(ModelNotFoundException::class); + + $regis = $this->createUser(); + $vault = $this->createVault($regis->account); + $vault = $this->setPermissionInVault($regis, Vault::PERMISSION_EDIT, $vault); + $contact = Contact::factory()->create(); + + $this->executeService($regis, $regis->account, $vault, $contact); + } + + /** @test */ + public function it_fails_if_user_doesnt_have_right_permission_in_initial_vault(): void + { + config(['services.uploadcare.public_key' => 'test']); + config(['services.uploadcare.private_key' => 'test']); + + $this->expectException(NotEnoughPermissionException::class); + + $regis = $this->createUser(); + $vault = $this->createVault($regis->account); + $vault = $this->setPermissionInVault($regis, Vault::PERMISSION_VIEW, $vault); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + + $this->executeService($regis, $regis->account, $vault, $contact); + } + + private function executeService(User $author, Account $account, Vault $vault, Contact $contact): void + { + $request = [ + 'account_id' => $account->id, + 'vault_id' => $vault->id, + 'author_id' => $author->id, + 'contact_id' => $contact->id, + 'uuid' => '017162da-e83b-46fc-89fc-3a7740db0a81', + 'name' => 'Twitter post.png', + 'original_url' => 'https://ucarecdn.com/5c8b9cea-62e5-4c8b-bc4c-47c0ddae62eee/', + 'cdn_url' => 'cdn_url', + 'mime_type' => 'image/jpg', + 'size' => 390340, + 'type' => 'avatar', + ]; + + $file = (new UploadFile())->execute($request); + + $this->assertInstanceOf( + File::class, + $file + ); + + $this->assertDatabaseHas('files', [ + 'id' => $file->id, + 'contact_id' => $contact->id, + 'uuid' => '017162da-e83b-46fc-89fc-3a7740db0a81', + 'name' => 'Twitter post.png', + 'original_url' => 'https://ucarecdn.com/5c8b9cea-62e5-4c8b-bc4c-47c0ddae62eee/', + 'cdn_url' => 'cdn_url', + 'mime_type' => 'image/jpg', + 'size' => 390340, + 'type' => 'avatar', + ]); + } +} diff --git a/tests/Unit/Domains/Contact/ManageDocuments/Web/ViewHelpers/ModuleDocumentsViewHelperTest.php b/tests/Unit/Domains/Contact/ManageDocuments/Web/ViewHelpers/ModuleDocumentsViewHelperTest.php new file mode 100644 index 00000000000..645442f4ea4 --- /dev/null +++ b/tests/Unit/Domains/Contact/ManageDocuments/Web/ViewHelpers/ModuleDocumentsViewHelperTest.php @@ -0,0 +1,75 @@ + '123']); + + $contact = Contact::factory()->create(); + File::factory()->create([ + 'contact_id' => $contact->id, + ]); + + $array = ModuleDocumentsViewHelper::data($contact); + + $this->assertEquals( + 4, + count($array) + ); + + $this->assertArrayHasKey('documents', $array); + $this->assertArrayHasKey('uploadcarePublicKey', $array); + $this->assertArrayHasKey('canUploadFile', $array); + $this->assertArrayHasKey('url', $array); + + $this->assertEquals( + '123', + $array['uploadcarePublicKey'] + ); + $this->assertFalse($array['canUploadFile']); + $this->assertEquals( + [ + 'store' => env('APP_URL') . '/vaults/' . $contact->vault->id . '/contacts/' . $contact->id . '/documents', + ], + $array['url'] + ); + } + + /** @test */ + public function it_gets_the_data_transfer_object(): void + { + $contact = Contact::factory()->create(); + $file = File::factory()->create([ + 'contact_id' => $contact->id, + 'size' => 123, + ]); + + $array = ModuleDocumentsViewHelper::dto($file, $contact); + + $this->assertEquals( + [ + 'id' => $file->id, + 'download_url' => $file->cdn_url, + 'name' => $file->name, + 'mime_type' => $file->mime_type, + 'size' => '123B', + 'url' => [ + 'destroy' => env('APP_URL') . '/vaults/' . $contact->vault->id . '/contacts/' . $contact->id . '/documents/'.$file->id, + ], + ], + $array + ); + } +} diff --git a/tests/Unit/Domains/Settings/CancelAccount/Services/CancelAccountTest.php b/tests/Unit/Domains/Settings/CancelAccount/Services/CancelAccountTest.php index d4fd318d4aa..0d9d932750a 100644 --- a/tests/Unit/Domains/Settings/CancelAccount/Services/CancelAccountTest.php +++ b/tests/Unit/Domains/Settings/CancelAccount/Services/CancelAccountTest.php @@ -3,10 +3,13 @@ namespace Tests\Unit\Domains\Settings\CancelAccount\Services; use App\Models\Account; +use App\Models\Contact; use App\Models\User; +use App\Models\File; use App\Settings\CancelAccount\Services\CancelAccount; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Queue; use Illuminate\Validation\ValidationException; use Tests\TestCase; @@ -19,7 +22,13 @@ class CancelAccountTest extends TestCase public function it_destroys_an_account(): void { $user = $this->createAdministrator(); - $this->executeService($user->account, $user); + $vault = $this->createVault($user->account); + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $file = File::factory()->create([ + 'contact_id' => $contact->id, + ]); + + $this->executeService($user->account, $user, $file); } /** @test */ @@ -53,9 +62,10 @@ public function it_fails_if_wrong_parameters_are_given(): void (new CancelAccount())->execute($request); } - private function executeService(Account $account, User $user): void + private function executeService(Account $account, User $user, File $file = null): void { Queue::fake(); + Event::fake(); $request = [ 'account_id' => $account->id, @@ -67,5 +77,9 @@ private function executeService(Account $account, User $user): void $this->assertDatabaseMissing('accounts', [ 'id' => $account->id, ]); + + $this->assertDatabaseMissing('files', [ + 'id' => $file->id, + ]); } } diff --git a/tests/Unit/Domains/Settings/CreateAccount/Services/CreateAccountTest.php b/tests/Unit/Domains/Settings/CreateAccount/Services/CreateAccountTest.php index 5c14abc550c..41e5521efe5 100644 --- a/tests/Unit/Domains/Settings/CreateAccount/Services/CreateAccountTest.php +++ b/tests/Unit/Domains/Settings/CreateAccount/Services/CreateAccountTest.php @@ -35,6 +35,7 @@ public function it_fails_if_wrong_parameters_are_given(): void private function executeService(): void { Queue::fake(); + config(['monica.default_storage_limit_in_mb' => 120]); $request = [ 'first_name' => 'john', @@ -47,6 +48,7 @@ private function executeService(): void $this->assertDatabaseHas('accounts', [ 'id' => $user->account->id, + 'storage_limit_in_mb' => 120, ]); $this->assertDatabaseHas('users', [ diff --git a/tests/Unit/Domains/Settings/ManageSettings/Web/ViewHelpers/SettingsIndexViewHelperTest.php b/tests/Unit/Domains/Settings/ManageSettings/Web/ViewHelpers/SettingsIndexViewHelperTest.php index 63ba2e74751..c3046d54aea 100644 --- a/tests/Unit/Domains/Settings/ManageSettings/Web/ViewHelpers/SettingsIndexViewHelperTest.php +++ b/tests/Unit/Domains/Settings/ManageSettings/Web/ViewHelpers/SettingsIndexViewHelperTest.php @@ -35,6 +35,9 @@ public function it_gets_the_data_needed_for_the_view(): void 'personalize' => [ 'index' => env('APP_URL').'/settings/personalize', ], + 'storage' => [ + 'index' => env('APP_URL').'/settings/storage', + ], 'cancel' => [ 'index' => env('APP_URL').'/settings/cancel', ], diff --git a/tests/Unit/Domains/Settings/ManageStorage/Web/ViewHelpers/StorageIndexViewHelperTest.php b/tests/Unit/Domains/Settings/ManageStorage/Web/ViewHelpers/StorageIndexViewHelperTest.php new file mode 100644 index 00000000000..c9a43655042 --- /dev/null +++ b/tests/Unit/Domains/Settings/ManageStorage/Web/ViewHelpers/StorageIndexViewHelperTest.php @@ -0,0 +1,77 @@ +create([ + 'storage_limit_in_mb' => 1, + ]); + $vault = Vault::factory()->create([ + 'account_id' => $account->id, + ]); + $contact = Contact::factory()->create([ + 'vault_id' => $vault->id, + ]); + File::factory()->create([ + 'contact_id' => $contact->id, + 'size' => 1024 * 1024, + 'type' => File::TYPE_AVATAR, + ]); + File::factory()->create([ + 'contact_id' => $contact->id, + 'size' => 1024 * 1024, + 'type' => File::TYPE_DOCUMENT, + ]); + File::factory()->create([ + 'contact_id' => $contact->id, + 'size' => 1024 * 1024, + 'type' => File::TYPE_PHOTO, + ]); + + $this->assertEquals( + [ + 'statistics' => [ + 'total' => '3MB', + 'total_percent' => 300, + 'photo' => [ + 'total' => 1, + 'total_percent' => 33, + 'size' => '1MB', + ], + 'document' => [ + 'total' => 1, + 'total_percent' => 33, + 'size' => '1MB', + ], + 'avatar' => [ + 'total' =>1, + 'total_percent' => 33, + 'size' => '1MB', + ], + ], + 'account_limit' => '1MB', + 'url' => [ + 'settings' => [ + 'index' => env('APP_URL') . '/settings', + ], + ], + ], + StorageIndexViewHelper::data($account) + ); + } +} diff --git a/tests/Unit/Domains/Vault/ManageFiles/Web/ViewHelpers/VaultFileIndexViewHelperTest.php b/tests/Unit/Domains/Vault/ManageFiles/Web/ViewHelpers/VaultFileIndexViewHelperTest.php new file mode 100644 index 00000000000..e846c2edd53 --- /dev/null +++ b/tests/Unit/Domains/Vault/ManageFiles/Web/ViewHelpers/VaultFileIndexViewHelperTest.php @@ -0,0 +1,75 @@ +create(); + $vault = Vault::factory()->create([ + 'account_id' => $user->account_id, + ]); + $contact = Contact::factory()->create([ + 'vault_id' => $vault->id, + ]); + $avatar = Avatar::factory()->create([ + 'contact_id' => $contact->id, + ]); + $contact->avatar_id = $avatar->id; + $contact->save(); + $file = File::factory()->create([ + 'contact_id' => $contact->id, + 'type' => File::TYPE_DOCUMENT, + 'created_at' => '2022-01-01', + 'size' => '12345', + ]); + + $files = File::all(); + + $array = VaultFileIndexViewHelper::data($files, $user, $vault); + $this->assertCount(2, $array); + $this->assertArrayHasKey('files', $array); + $this->assertArrayHasKey('statistics', $array); + + $this->assertEquals( + [ + 0 => [ + 'id' => $file->id, + 'download_url' => $file->cdn_url, + 'name' => $file->name, + 'mime_type' => $file->mime_type, + 'size' => '12.06kB', + 'created_at' => 'Jan 01, 2022', + 'url' => [ + 'destroy' => env('APP_URL') . '/vaults/' . $contact->vault->id . '/contacts/' . $contact->id . '/documents/' . $file->id, + ], + 'contact' => [ + 'id' => $contact->id, + 'name' => $contact->name, + 'avatar' => '123', + 'url' => [ + 'show' => env('APP_URL') . '/vaults/' . $vault->id . '/contacts/' . $contact->id, + ], + ], + ], + ], + $array['files']->toArray() + ); + } +} diff --git a/tests/Unit/Domains/Vault/ManageVault/Services/DestroyVaultTest.php b/tests/Unit/Domains/Vault/ManageVault/Services/DestroyVaultTest.php index 690eced8dcf..548342f3b35 100644 --- a/tests/Unit/Domains/Vault/ManageVault/Services/DestroyVaultTest.php +++ b/tests/Unit/Domains/Vault/ManageVault/Services/DestroyVaultTest.php @@ -4,11 +4,14 @@ use App\Exceptions\NotEnoughPermissionException; use App\Models\Account; +use App\Models\Contact; +use App\Models\File; use App\Models\User; use App\Models\Vault; use App\Vault\ManageVault\Services\DestroyVault; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Queue; use Illuminate\Validation\ValidationException; use Tests\TestCase; @@ -23,7 +26,13 @@ public function it_destroys_a_vault(): void $ross = $this->createUser(); $vault = $this->createVault($ross->account); $vault = $this->setPermissionInVault($ross, Vault::PERMISSION_MANAGE, $vault); - $this->executeService($ross, $ross->account, $vault); + + $contact = Contact::factory()->create(['vault_id' => $vault->id]); + $file = File::factory()->create([ + 'contact_id' => $contact->id, + ]); + + $this->executeService($ross, $ross->account, $vault, $file); } /** @test */ @@ -71,9 +80,10 @@ public function it_fails_if_user_doesnt_have_right_permission_in_vault(): void $this->executeService($ross, $ross->account, $vault); } - private function executeService(User $author, Account $account, Vault $vault): void + private function executeService(User $author, Account $account, Vault $vault, File $file = null): void { Queue::fake(); + Event::fake(); $request = [ 'account_id' => $account->id, @@ -86,5 +96,9 @@ private function executeService(User $author, Account $account, Vault $vault): v $this->assertDatabaseMissing('vaults', [ 'id' => $vault->id, ]); + + $this->assertDatabaseMissing('files', [ + 'id' => $file->id, + ]); } } diff --git a/tests/Unit/Domains/Vault/ManageVault/Web/ViewHelpers/VaultIndexViewHelperTest.php b/tests/Unit/Domains/Vault/ManageVault/Web/ViewHelpers/VaultIndexViewHelperTest.php index e4f7efbf42e..b3897249713 100644 --- a/tests/Unit/Domains/Vault/ManageVault/Web/ViewHelpers/VaultIndexViewHelperTest.php +++ b/tests/Unit/Domains/Vault/ManageVault/Web/ViewHelpers/VaultIndexViewHelperTest.php @@ -46,6 +46,7 @@ public function it_gets_general_layout_information(): void 'url' => [ 'dashboard' => env('APP_URL').'/vaults/'.$vault->id, 'contacts' => env('APP_URL').'/vaults/'.$vault->id.'/contacts', + 'files' => env('APP_URL').'/vaults/'.$vault->id.'/files', 'settings' => env('APP_URL').'/vaults/'.$vault->id.'/settings', 'search' => env('APP_URL').'/vaults/'.$vault->id.'/search', 'get_most_consulted_contacts' => env('APP_URL').'/vaults/'.$vault->id.'/search/user/contact/mostConsulted', diff --git a/tests/Unit/Helpers/FileHelperTest.php b/tests/Unit/Helpers/FileHelperTest.php new file mode 100644 index 00000000000..7908902881a --- /dev/null +++ b/tests/Unit/Helpers/FileHelperTest.php @@ -0,0 +1,25 @@ +assertEquals( + '12.61MB', + FileHelper::formatFileSize(13223239) + ); + $this->assertEquals( + '12.93kB', + FileHelper::formatFileSize(13240) + ); + } +} diff --git a/tests/Unit/Helpers/StorageHelperTest.php b/tests/Unit/Helpers/StorageHelperTest.php new file mode 100644 index 00000000000..eb026e0bfb9 --- /dev/null +++ b/tests/Unit/Helpers/StorageHelperTest.php @@ -0,0 +1,39 @@ +create([ + 'storage_limit_in_mb' => 1, + ]); + + $this->assertTrue(StorageHelper::canUploadFile($account)); + + $vault = Vault::factory()->create([ + 'account_id' => $account->id, + ]); + $contact = Contact::factory()->create([ + 'vault_id' => $vault->id, + ]); + File::factory()->create([ + 'contact_id' => $contact->id, + 'size' => 1024 * 1024, + ]); + + $this->assertFalse(StorageHelper::canUploadFile($account)); + } +} diff --git a/tests/Unit/Models/AccountTest.php b/tests/Unit/Models/AccountTest.php index 6d1423c33d4..978e665fb88 100644 --- a/tests/Unit/Models/AccountTest.php +++ b/tests/Unit/Models/AccountTest.php @@ -19,6 +19,7 @@ use App\Models\RelationshipGroupType; use App\Models\Template; use App\Models\User; +use App\Models\Vault; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; @@ -201,4 +202,15 @@ public function it_has_many_gift_states() $this->assertTrue($account->giftStates()->exists()); } + + /** @test */ + public function it_has_many_vaults() + { + $account = Account::factory()->create(); + Vault::factory(2)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($account->vaults()->exists()); + } } diff --git a/tests/Unit/Models/ContactTest.php b/tests/Unit/Models/ContactTest.php index 12b27c29f56..b586b5c24cd 100644 --- a/tests/Unit/Models/ContactTest.php +++ b/tests/Unit/Models/ContactTest.php @@ -24,6 +24,7 @@ use App\Models\RelationshipType; use App\Models\Template; use App\Models\User; +use App\Models\File; use Carbon\Carbon; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; @@ -248,6 +249,17 @@ public function it_has_many_goals(): void $this->assertTrue($ross->goals()->exists()); } + /** @test */ + public function it_has_many_files(): void + { + $ross = Contact::factory()->create(); + File::factory()->count(2)->create([ + 'contact_id' => $ross->id, + ]); + + $this->assertTrue($ross->files()->exists()); + } + /** @test */ public function it_has_many_groups(): void { diff --git a/tests/Unit/Models/FileTest.php b/tests/Unit/Models/FileTest.php new file mode 100644 index 00000000000..64deba78dc8 --- /dev/null +++ b/tests/Unit/Models/FileTest.php @@ -0,0 +1,20 @@ +create(); + + $this->assertTrue($file->contact()->exists()); + } +} diff --git a/yarn.lock b/yarn.lock index b2093e25afc..89401ef12db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3136,7 +3136,7 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= @@ -4329,6 +4329,11 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" +jquery@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" + integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6724,6 +6729,21 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= +uploadcare-vue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/uploadcare-vue/-/uploadcare-vue-1.0.0.tgz#f0b914994741e3a9599ca17bf3e7950b058e51f2" + integrity sha512-SpoNTsCSKpMXVQyF5y+phk62nhkR2WZ1ZUdnk6aiNif6woVVIodqJK806BMuVHOmqGnx6u0A3mpD4Phoe5UuSg== + dependencies: + uploadcare-widget "^3.3.0" + +uploadcare-widget@^3.3.0: + version "3.17.2" + resolved "https://registry.yarnpkg.com/uploadcare-widget/-/uploadcare-widget-3.17.2.tgz#5e09d8c0000eb3653afd9a7ff1d502fb874bbcf7" + integrity sha512-A9fDKCguevWcFaMCrHROtYzwnMZbOozER6Dru+4zrZH1pP0S7zI/g+7wLv+5U4XZTc5m+4ZrwHrZJzkIRnWeMQ== + dependencies: + escape-html "^1.0.3" + jquery "^3.6.0" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"