├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── UPGRADING.md ├── app ├── Actions │ ├── AcceptCollaborationInvite.php │ ├── CreateCollaborationInvite.php │ ├── CreateUser.php │ ├── CreateVault.php │ ├── CreateVaultNode.php │ ├── DeclineCollaborationInvite.php │ ├── DeleteCollaborationInvite.php │ ├── DeleteVault.php │ ├── DeleteVaultNode.php │ ├── ExportVault.php │ ├── GetAvailableOAuthProviders.php │ ├── GetPathFromUser.php │ ├── GetPathFromVault.php │ ├── GetPathFromVaultNode.php │ ├── GetUrlFromVaultNode.php │ ├── GetVaultNodeFromPath.php │ ├── ProcessDiskVault.php │ ├── ProcessImportedFile.php │ ├── ProcessImportedVault.php │ ├── ProcessVaultLinks.php │ ├── ProcessVaultNodeLinks.php │ ├── ProcessVaultNodeTags.php │ ├── ProcessVaultTags.php │ ├── ResolveTwoPaths.php │ ├── UpdateVault.php │ └── UpdateVaultNode.php ├── Console │ └── Commands │ │ └── Upgrade │ │ ├── CreateStartupVaultCommand.php │ │ ├── ProcessLinksCommand.php │ │ ├── ReimportDataIntoTypesenseCommand.php │ │ ├── RunCommand.php │ │ └── SyncDatabaseNotesCommand.php ├── Enums │ └── OAuthProviders.php ├── Events │ ├── CollaborationAcceptedEvent.php │ ├── CollaborationDeclinedEvent.php │ ├── CollaborationDeletedEvent.php │ ├── UserNotifiedEvent.php │ ├── VaultFileSystemUpdatedEvent.php │ ├── VaultListUpdatedEvent.php │ ├── VaultNodeDeletedEvent.php │ └── VaultNodeUpdatedEvent.php ├── Http │ └── Controllers │ │ └── FileController.php ├── Livewire │ ├── Auth │ │ ├── ForgotPassword.php │ │ ├── Login.php │ │ ├── OAuthLogin.php │ │ ├── OAuthLoginCallback.php │ │ ├── Register.php │ │ └── ResetPassword.php │ ├── Dashboard │ │ └── Index.php │ ├── Forms │ │ ├── CollaborationInviteForm.php │ │ ├── EditPasswordForm.php │ │ ├── EditProfileForm.php │ │ ├── ForgotPasswordForm.php │ │ ├── LoginForm.php │ │ ├── RegisterForm.php │ │ ├── ResetPasswordForm.php │ │ ├── VaultForm.php │ │ └── VaultNodeForm.php │ ├── Layout │ │ ├── NotificationMenu.php │ │ └── UserMenu.php │ ├── Modals │ │ ├── AddNode.php │ │ ├── Collaboration.php │ │ ├── CollaborationInvite.php │ │ ├── EditNode.php │ │ ├── ImportFile.php │ │ ├── ImportVault.php │ │ ├── MarkdownEditorSearch.php │ │ ├── MarkdownEditorTemplate.php │ │ ├── Modal.php │ │ └── SearchNode.php │ └── Vault │ │ ├── Index.php │ │ ├── Row.php │ │ ├── Show.php │ │ └── TreeView.php ├── Models │ ├── Tag.php │ ├── User.php │ ├── Vault.php │ └── VaultNode.php ├── Notifications │ ├── CollaborationAccepted.php │ ├── CollaborationDeclined.php │ └── CollaborationInvited.php ├── Policies │ ├── VaultNodePolicy.php │ └── VaultPolicy.php ├── Providers │ └── AppServiceProvider.php └── Services │ ├── VaultFile.php │ └── VaultFiles │ ├── Audio.php │ ├── File.php │ ├── Image.php │ ├── Note.php │ ├── Pdf.php │ └── Video.php ├── art ├── logo.png ├── theme-dark.png └── theme-light.png ├── artisan ├── assets └── Starter Vault │ ├── 00 Resources │ └── Templates │ │ └── Basic.md │ ├── 01 Inbox │ ├── Features & Tips.md │ ├── Next Step.md │ └── Organizational Methods.md │ ├── 02 Personal │ └── Goals.md │ ├── 04 Projects │ └── 04.01 Example Project │ │ └── Project Ideas.md │ └── Start Here.md ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── broadcasting.php ├── cache.php ├── database.php ├── filesystems.php ├── livewire.php ├── logging.php ├── mail.php ├── queue.php ├── reverb.php ├── scout.php ├── services.php ├── session.php └── settings.php ├── database ├── .gitignore ├── factories │ ├── TagFactory.php │ ├── UserFactory.php │ ├── VaultFactory.php │ └── VaultNodeFactory.php ├── migrations │ ├── 0001_01_01_000000_create_users_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 0001_01_01_000002_create_jobs_table.php │ ├── 2024_07_16_025200_create_vaults_table.php │ ├── 2024_08_16_010335_create_vault_nodes_table.php │ ├── 2024_12_17_132832_add_templates_node_id_to_vaults_table.php │ ├── 2024_12_23_150046_remove_soft_deletes_from_vaults.php │ ├── 2024_12_23_151515_remove_soft_deletes_from_vault_nodes.php │ ├── 2025_02_06_131404_create_vault_node_vault_node_table.php │ ├── 2025_02_08_192143_upgrades.php │ ├── 2025_02_10_190148_create_tags_table.php │ ├── 2025_02_10_190644_create_tag_vault_node_table.php │ ├── 2025_03_13_200017_create_notifications_table.php │ ├── 2025_03_22_152303_create_user_vault_table.php │ ├── 2025_03_31_181055_remove_opened_at_field_from_vaults_table.php │ └── 2025_03_31_181654_add_last_visited_url_to_users_table.php └── seeders │ └── DatabaseSeeder.php ├── deploy └── docker │ ├── entrypoint │ └── 75-upgrades.sh │ ├── nginx │ └── reverb.conf │ └── s6-overlay │ ├── reverb │ ├── dependencies │ ├── finish │ ├── run │ └── type │ ├── typesense │ ├── dependencies │ ├── finish │ ├── run │ └── type │ └── user │ ├── reverb │ └── typesense ├── docs ├── customization │ └── oauth.md ├── installation │ ├── docker-bind-mounts.md │ ├── docker-different-database.md │ └── non-docker.md └── support │ └── faqs.md ├── package-lock.json ├── package.json ├── phpstan.neon ├── phpunit.xml ├── pint.json ├── public ├── .htaccess ├── assets │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── favicon.svg │ ├── logo.png │ └── site.webmanifest ├── index.php └── robots.txt ├── rector.php ├── resources ├── css │ └── app.css ├── js │ ├── app.js │ ├── bootstrap.js │ └── echo.js └── views │ ├── components │ ├── form │ │ ├── button.blade.php │ │ ├── checkbox.blade.php │ │ ├── index.blade.php │ │ ├── input.blade.php │ │ ├── link.blade.php │ │ ├── linkButton.blade.php │ │ ├── sessionError.blade.php │ │ ├── sessionStatus.blade.php │ │ ├── submit.blade.php │ │ └── text.blade.php │ ├── icons │ │ ├── arrowDownOnSquare.blade.php │ │ ├── arrowDownTray.blade.php │ │ ├── arrowRightEndOnRectangle.blade.php │ │ ├── arrowRightStartOnRectangle.blade.php │ │ ├── arrowUpOnSquare.blade.php │ │ ├── arrowUpTray.blade.php │ │ ├── bars3.blade.php │ │ ├── bars3BottomLeft.blade.php │ │ ├── bars3BottomRight.blade.php │ │ ├── bell.blade.php │ │ ├── bookOpen.blade.php │ │ ├── checkCircle.blade.php │ │ ├── chevronDown.blade.php │ │ ├── chevronRight.blade.php │ │ ├── circleStack.blade.php │ │ ├── codeBracket.blade.php │ │ ├── documentDuplicate.blade.php │ │ ├── documentPlus.blade.php │ │ ├── ellipsisVertical.blade.php │ │ ├── exclamationCircle.blade.php │ │ ├── exclamationTriangle.blade.php │ │ ├── folder.blade.php │ │ ├── folderPlus.blade.php │ │ ├── informationCircle.blade.php │ │ ├── lockClosed.blade.php │ │ ├── magnifyingGlass.blade.php │ │ ├── pencilSquare.blade.php │ │ ├── plus.blade.php │ │ ├── questionMarkCircle.blade.php │ │ ├── spinner.blade.php │ │ ├── trash.blade.php │ │ ├── user.blade.php │ │ ├── userGroup.blade.php │ │ └── xMark.blade.php │ ├── layouts │ │ ├── app.blade.php │ │ ├── appHeader.blade.php │ │ ├── appMain.blade.php │ │ └── guestMain.blade.php │ ├── markdownEditor │ │ ├── button.blade.php │ │ ├── index.blade.php │ │ ├── itemDivider.blade.php │ │ ├── itemDropdown.blade.php │ │ ├── items.blade.php │ │ ├── subButton.blade.php │ │ └── toolbar.blade.php │ ├── menu │ │ ├── button.blade.php │ │ ├── close.blade.php │ │ ├── index.blade.php │ │ ├── item.blade.php │ │ ├── itemDivider.blade.php │ │ ├── itemLink.blade.php │ │ └── items.blade.php │ ├── modal │ │ ├── close.blade.php │ │ ├── index.blade.php │ │ ├── open.blade.php │ │ └── panel.blade.php │ ├── notification │ │ ├── collaborationAccepted.blade.php │ │ ├── collaborationDeclined.blade.php │ │ └── collaborationInvited.blade.php │ ├── toast │ │ └── index.blade.php │ ├── treeView │ │ ├── badge.blade.php │ │ ├── index.blade.php │ │ ├── item.blade.php │ │ ├── itemFile.blade.php │ │ ├── itemFolder.blade.php │ │ └── items.blade.php │ └── vault │ │ ├── treeViewNode.blade.php │ │ └── treeViewRow.blade.php │ └── livewire │ ├── auth │ ├── forgot-password.blade.php │ ├── login.blade.php │ ├── register.blade.php │ └── reset-password.blade.php │ ├── layout │ ├── notificationMenu.blade.php │ └── userMenu.blade.php │ ├── modals │ ├── addNode.blade.php │ ├── collaboration.blade.php │ ├── collaborationInvite.blade.php │ ├── editNode.blade.php │ ├── importFile.blade.php │ ├── importVault.blade.php │ ├── markdownEditorSearch.blade.php │ ├── markdownEditorTemplate.blade.php │ └── searchNode.blade.php │ └── vault │ ├── index.blade.php │ ├── row.blade.php │ ├── show.blade.php │ └── treeView.blade.php ├── routes ├── channels.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ ├── private │ │ └── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tests ├── Feature │ ├── Auth │ │ ├── DashboardTest.php │ │ ├── ForgotPasswordTest.php │ │ ├── LoginTest.php │ │ ├── OAuthLoginCallbackTest.php │ │ ├── OAuthLoginTest.php │ │ ├── RegisterTest.php │ │ └── ResetPasswordTest.php │ ├── Controllers │ │ └── FileTest.php │ ├── Layout │ │ └── UserMenuTest.php │ ├── Modals │ │ ├── AddNodeTest.php │ │ ├── CollaborationInviteTest.php │ │ ├── EditNodeTest.php │ │ ├── ImportFileTest.php │ │ ├── ImportVaultTest.php │ │ ├── MarkdownEditorSearchTest.php │ │ ├── MarkdownEditorTemplateTest.php │ │ └── SearchNodeTest.php │ └── Vault │ │ ├── IndexTest.php │ │ ├── RowTest.php │ │ ├── ShowTest.php │ │ └── TreeViewTest.php ├── Pest.php ├── TestCase.php └── Unit │ ├── Actions │ ├── DeleteVaultNodeTest.php │ ├── DeleteVaultTest.php │ ├── ExportVaultTest.php │ ├── ResolveTwoPathsTest.php │ └── UpdateVaultNodeTest.php │ ├── ArchTest.php │ ├── Console │ └── UpgradeTest.php │ └── Models │ ├── TagTest.php │ ├── UserTest.php │ ├── VaultNodeTest.php │ └── VaultTest.php ├── typesense └── .gitignore └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="Many Notes" 2 | APP_ENV=production 3 | APP_KEY= 4 | APP_DEBUG=false 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://localhost 7 | 8 | APP_LOCALE=en 9 | APP_FALLBACK_LOCALE=en 10 | APP_FAKER_LOCALE=en_US 11 | 12 | APP_MAINTENANCE_DRIVER=file 13 | APP_MAINTENANCE_STORE=database 14 | 15 | BCRYPT_ROUNDS=12 16 | 17 | LOG_CHANNEL=stack 18 | LOG_STACK=single 19 | LOG_DEPRECATIONS_CHANNEL=null 20 | LOG_LEVEL=debug 21 | 22 | DB_CONNECTION=sqlite 23 | # DB_HOST=127.0.0.1 24 | # DB_PORT=3306 25 | # DB_DATABASE=laravel 26 | # DB_USERNAME=root 27 | # DB_PASSWORD= 28 | 29 | SESSION_DRIVER=database 30 | SESSION_LIFETIME=120 31 | SESSION_ENCRYPT=false 32 | SESSION_PATH=/ 33 | SESSION_DOMAIN=null 34 | 35 | BROADCAST_CONNECTION=log 36 | FILESYSTEM_DISK=local 37 | QUEUE_CONNECTION=database 38 | 39 | CACHE_STORE=database 40 | CACHE_PREFIX= 41 | 42 | MEMCACHED_HOST=127.0.0.1 43 | 44 | REDIS_CLIENT=phpredis 45 | REDIS_HOST=127.0.0.1 46 | REDIS_PASSWORD=null 47 | REDIS_PORT=6379 48 | 49 | MAIL_MAILER=log 50 | MAIL_HOST=127.0.0.1 51 | MAIL_PORT=2525 52 | MAIL_USERNAME=null 53 | MAIL_PASSWORD=null 54 | MAIL_ENCRYPTION=null 55 | MAIL_FROM_ADDRESS="hello@example.com" 56 | MAIL_FROM_NAME="${APP_NAME}" 57 | 58 | AWS_ACCESS_KEY_ID= 59 | AWS_SECRET_ACCESS_KEY= 60 | AWS_DEFAULT_REGION=us-east-1 61 | AWS_BUCKET= 62 | AWS_USE_PATH_STYLE_ENDPOINT=false 63 | 64 | VITE_APP_NAME="${APP_NAME}" 65 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /vendor 8 | .env 9 | .env.backup 10 | .env.production 11 | .phpactor.json 12 | .phpunit.result.cache 13 | Homestead.json 14 | Homestead.yaml 15 | auth.json 16 | npm-debug.log 17 | yarn-error.log 18 | /.fleet 19 | /.idea 20 | /.vscode 21 | /storage/debugbar 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 brufdev 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 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrade guide 2 | 3 | This guide will walk you through the necessary changes to upgrade from a previous version of Many Notes. If you need further help, consult the [FAQs](docs/support/faqs.md), or if your question isn't answered there, please open an issue on GitHub. 4 | 5 | ## Upgrading from any version below 0.9 6 | 7 | Version 0.9 introduces an improved search feature powered by Typesense, which requires mounting a new directory to persist Typesense data when the container is down. Additionally, the `compose.yaml` file has been simplified, as the `ASSET_URL` environment variable and the `public` and `sessions` directories are no longer needed. 8 | 9 | If you upgraded without mounting the Typesense directory, the search feature will return no results after the next container restart. Please refer to the [FAQs](docs/support/faqs.md) for instructions on how to resolve this issue. 10 | 11 | ## Upgrading from any version below 0.7 12 | 13 | Version 0.7 changes the location of the SQLite database file to support both Docker volumes and bind mounts. The database file is now located in the `database/sqlite` directory. 14 | 15 | ## Upgrading from any version below 0.4 16 | 17 | Version 0.4 introduces **breaking changes** in how the vaults are saved. Stop the containers and back up your data before proceeding. 18 | 19 | Notes were only saved in the database, but starting from v0.4, they are also saved in the filesystem. In case of database corruption, the `private` directory will now contain a complete copy of all vaults. 20 | 21 | The installation instructions now recommend using bind mounts instead of Docker volumes, and SQLite instead of MariaDB. This change is intended to simplify the installation process. However, you can still use Docker volumes or MariaDB if you prefer. 22 | 23 | The best way to upgrade is to export all vaults from the UI in v0.3 and then import them after a fresh installation of the new version. If you updated the Docker image before exporting the vaults, you can downgrade to v0.3 by using the corresponding tag. 24 | -------------------------------------------------------------------------------- /app/Actions/AcceptCollaborationInvite.php: -------------------------------------------------------------------------------- 1 | collaborators() 16 | ->wherePivot('user_id', $user->id) 17 | ->wherePivot('accepted', false) 18 | ->exists(); 19 | 20 | if (!$inviteExists) { 21 | return false; 22 | } 23 | 24 | $vault->collaborators()->updateExistingPivot($user->id, ['accepted' => 1]); 25 | $notifications = $user->notifications()->where('type', CollaborationInvited::class)->get(); 26 | 27 | foreach ($notifications as $notification) { 28 | if ($notification->data['vault_id'] === $vault->id) { 29 | $notification->delete(); 30 | } 31 | } 32 | 33 | return true; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Actions/CreateCollaborationInvite.php: -------------------------------------------------------------------------------- 1 | collaborators()->wherePivot('user_id', $user->id)->count()) { 15 | return; 16 | } 17 | 18 | $vault->collaborators()->attach($user, ['accepted' => 0]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Actions/CreateUser.php: -------------------------------------------------------------------------------- 1 | $attributes 13 | */ 14 | public function handle(array $attributes): User 15 | { 16 | $user = User::create($attributes); 17 | new ProcessDiskVault()->handle($user, base_path('assets/Starter Vault')); 18 | 19 | return $user; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Actions/CreateVault.php: -------------------------------------------------------------------------------- 1 | vaults() 20 | ->where('name', 'like', $attributes['name']) 21 | ->exists(); 22 | 23 | if ($vaultExists) { 24 | /** @var list $vaults */ 25 | $vaults = $user->vaults() 26 | ->select('name') 27 | ->where('name', 'like', $attributes['name'] . '-%') 28 | ->pluck('name') 29 | ->toArray(); 30 | natcasesort($vaults); 31 | $attributes['name'] .= count($vaults) && preg_match('/-(\d+)$/', end($vaults), $matches) === 1 32 | ? '-' . ((int) $matches[1] + 1) 33 | : '-1'; 34 | } 35 | 36 | // Save vault to database 37 | $vault = $user->vaults()->create($attributes); 38 | 39 | // Save vault to disk 40 | $vaultPath = new GetPathFromVault()->handle($vault); 41 | Storage::disk('local')->makeDirectory($vaultPath); 42 | 43 | return $vault; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Actions/DeclineCollaborationInvite.php: -------------------------------------------------------------------------------- 1 | collaborators() 16 | ->wherePivot('user_id', $user->id) 17 | ->wherePivot('accepted', false) 18 | ->exists(); 19 | 20 | if (!$inviteExists) { 21 | return false; 22 | } 23 | 24 | $vault->collaborators()->detach($user->id); 25 | $notifications = $user->notifications()->where('type', CollaborationInvited::class)->get(); 26 | 27 | foreach ($notifications as $notification) { 28 | if ($notification->data['vault_id'] === $vault->id) { 29 | $notification->delete(); 30 | } 31 | } 32 | 33 | return true; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Actions/DeleteCollaborationInvite.php: -------------------------------------------------------------------------------- 1 | collaborators()->detach($user); 16 | $notifications = $user->notifications()->where('type', CollaborationInvited::class)->get(); 17 | 18 | foreach ($notifications as $notification) { 19 | if ($notification->data['vault_id'] === $vault->id) { 20 | $notification->delete(); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Actions/GetAvailableOAuthProviders.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function handle(): array 15 | { 16 | return array_filter( 17 | OAuthProviders::cases(), 18 | /** @phpstan-ignore-next-line */ 19 | fn (OAuthProviders $provider): ?string => config("services.{$provider->value}.client_id"), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Actions/GetPathFromUser.php: -------------------------------------------------------------------------------- 1 | id); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Actions/GetPathFromVault.php: -------------------------------------------------------------------------------- 1 | load('user')->user; 16 | 17 | return sprintf('private/vaults/%u/%s/', $user->id, $vault->name); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Actions/GetPathFromVaultNode.php: -------------------------------------------------------------------------------- 1 | load(['vault', 'parent'])->vault; 17 | /** @var User $user */ 18 | $user = $vault->user()->first(); 19 | $relativePath = ''; 20 | 21 | if ($node->parent) { 22 | /** 23 | * @var string $fullPath 24 | * 25 | * @phpstan-ignore-next-line larastan.noUnnecessaryCollectionCall 26 | */ 27 | $fullPath = $node->parent->ancestorsAndSelf()->get()->last()->full_path; 28 | $relativePath = $fullPath . '/'; 29 | } 30 | 31 | $path = sprintf( 32 | 'private/vaults/%u/%s/%s', 33 | $user->id, 34 | $vault->name, 35 | $relativePath, 36 | ); 37 | 38 | if ($includeSelf) { 39 | $path .= $node->name . ($node->is_file ? '.' . $node->extension : ''); 40 | } 41 | 42 | return $path; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Actions/GetUrlFromVaultNode.php: -------------------------------------------------------------------------------- 1 | ancestorsAndSelf()->get()->last()->full_path; 19 | 20 | return '/files/' . $node->vault_id . '?path=' . $fullPath . '.' . $node->extension; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Actions/GetVaultNodeFromPath.php: -------------------------------------------------------------------------------- 1 | where('vault_id', $vaultId) 22 | ->where('parent_id', $parentId) 23 | ->where('is_file', true) 24 | ->where('name', 'LIKE', $pathParts['filename']) 25 | ->where('extension', 'LIKE', $pathParts['extension'] ?? 'md') 26 | ->first(); 27 | } 28 | 29 | $node = VaultNode::query() 30 | ->where('vault_id', $vaultId) 31 | ->where('parent_id', $parentId) 32 | ->where('is_file', false) 33 | ->where('name', 'LIKE', $pieces[0]) 34 | ->first(); 35 | 36 | if (is_null($node)) { 37 | return $node; 38 | } 39 | 40 | return $this->handle($vaultId, Str::after($path, '/'), $node->id); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Actions/ProcessImportedFile.php: -------------------------------------------------------------------------------- 1 | $parent->id, 19 | 'is_file' => true, 20 | ]; 21 | $pathInfo = pathinfo($fileName); 22 | $attributes['name'] = $pathInfo['filename']; 23 | $attributes['extension'] = $pathInfo['extension'] ?? ''; 24 | $attributes['content'] = null; 25 | 26 | if (in_array($attributes['extension'], Note::extensions())) { 27 | $attributes['extension'] = 'md'; 28 | $attributes['content'] = (string) file_get_contents($filePath); 29 | } 30 | 31 | $node = new CreateVaultNode()->handle($vault, $attributes); 32 | 33 | if ($node->extension === 'md') { 34 | new ProcessVaultNodeLinks()->handle($node); 35 | new ProcessVaultNodeTags()->handle($node); 36 | } else { 37 | $relativePath = new GetPathFromVaultNode()->handle($node); 38 | $pathInfo = pathinfo($relativePath); 39 | $savePath = $pathInfo['dirname'] ?? ''; 40 | $saveName = $pathInfo['basename']; 41 | Storage::putFileAs($savePath, new File($filePath), $saveName); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Actions/ProcessVaultLinks.php: -------------------------------------------------------------------------------- 1 | nodes()->where('is_file', true)->where('extension', 'md')->get(); 15 | 16 | foreach ($nodes as $node) { 17 | $processVaultNodeLinks->handle($node); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Actions/ProcessVaultNodeTags.php: -------------------------------------------------------------------------------- 1 | tags()->detach(); 15 | 16 | if ((string) $node->content === '') { 17 | return; 18 | } 19 | 20 | /** @var string $content */ 21 | $content = $node->content; 22 | preg_match_all('/#[\w:-]+/', $content, $matches, PREG_OFFSET_CAPTURE); 23 | 24 | if ($matches[0] === []) { 25 | return; 26 | } 27 | 28 | foreach ($matches[0] as $match) { 29 | $tag = Tag::firstOrCreate([ 30 | 'name' => mb_substr($match[0], 1), 31 | ]); 32 | 33 | $node->tags()->attach($tag->id, ['position' => $match[1]]); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Actions/ProcessVaultTags.php: -------------------------------------------------------------------------------- 1 | nodes()->where('is_file', true)->where('extension', 'md')->get(); 15 | 16 | foreach ($nodes as $node) { 17 | $processVaultNodeTags->handle($node); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Actions/ResolveTwoPaths.php: -------------------------------------------------------------------------------- 1 | toArray(); 20 | $vault->update($attributes); 21 | 22 | if (!$vault->wasChanged('name')) { 23 | return; 24 | } 25 | 26 | /** @var User $user */ 27 | $user = $vault->user()->first(); 28 | $relativePath = new GetPathFromUser()->handle($user); 29 | Storage::disk('local')->move( 30 | $relativePath . $original['name'], 31 | $relativePath . $vault->name, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Actions/UpdateVaultNode.php: -------------------------------------------------------------------------------- 1 | handle($node); 23 | 24 | // Save node to database 25 | $node->update($attributes); 26 | 27 | // Save content to disk 28 | if ($node->is_file && in_array($node->extension, Note::extensions())) { 29 | Storage::disk('local')->put($originalPath, $attributes['content'] ?? ''); 30 | } 31 | 32 | // Rename node on disk 33 | if ($node->wasChanged(['name', 'parent_id'])) { 34 | $path = new GetPathFromVaultNode()->handle($node); 35 | Storage::disk('local')->move($originalPath, $path); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Console/Commands/Upgrade/CreateStartupVaultCommand.php: -------------------------------------------------------------------------------- 1 | handle($user, base_path('assets/Starter Vault')); 38 | } 39 | } catch (Throwable) { 40 | $this->error('Something went wrong processing the starter vault for existing users'); 41 | 42 | return self::FAILURE; 43 | } 44 | 45 | $this->info('All starter vaults were successfully processed'); 46 | 47 | return self::SUCCESS; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Console/Commands/Upgrade/ProcessLinksCommand.php: -------------------------------------------------------------------------------- 1 | handle($vault); 38 | } 39 | } catch (Throwable) { 40 | $this->error('Something went wrong processing links from existing vaults'); 41 | 42 | return self::FAILURE; 43 | } 44 | 45 | $this->info('All links were successfully processed'); 46 | 47 | return self::SUCCESS; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Console/Commands/Upgrade/ReimportDataIntoTypesenseCommand.php: -------------------------------------------------------------------------------- 1 | callSilent('scout:flush', ['model' => VaultNode::class]); 34 | $this->callSilent('scout:import', ['model' => VaultNode::class]); 35 | } catch (Throwable) { 36 | $this->error('Something went wrong reimporting existing data into Typesense'); 37 | 38 | return self::FAILURE; 39 | } 40 | 41 | $this->info('All data was successfully reimported into Typesense'); 42 | 43 | return self::SUCCESS; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Console/Commands/Upgrade/RunCommand.php: -------------------------------------------------------------------------------- 1 | executeCommands($commands); 39 | } 40 | 41 | /** 42 | * @param list $commands 43 | */ 44 | private function executeCommands(array $commands): void 45 | { 46 | $commandsExecuted = $this->getTotalCommandsExecuted(); 47 | $commandsTotal = count($commands); 48 | 49 | for ($i = $commandsExecuted; $i < $commandsTotal; $i++) { 50 | $this->call($commands[$i]); 51 | DB::table('upgrades')->update(['executed' => $i + 1]); 52 | } 53 | } 54 | 55 | private function getTotalCommandsExecuted(): int 56 | { 57 | if (DB::table('upgrades')->count() > 0) { 58 | /** @var object{executed: int} $upgrades */ 59 | $upgrades = DB::table('upgrades')->first(); 60 | 61 | return $upgrades->executed; 62 | } 63 | 64 | DB::table('upgrades')->insert(['executed' => 0]); 65 | 66 | return 0; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/Console/Commands/Upgrade/SyncDatabaseNotesCommand.php: -------------------------------------------------------------------------------- 1 | nodes() 39 | ->where('is_file', true) 40 | ->where('extension', 'md') 41 | ->get(); 42 | 43 | foreach ($nodes as $node) { 44 | $path = $getPathFromVaultNode->handle($node); 45 | Storage::disk('local')->put($path, $node->content ?? ''); 46 | } 47 | } 48 | } catch (Throwable) { 49 | $this->error('Something went wrong syncing database notes to disk'); 50 | 51 | return self::FAILURE; 52 | } 53 | 54 | $this->info('All notes were successfully synced to disk'); 55 | 56 | return self::SUCCESS; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Enums/OAuthProviders.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function broadcastOn(): array 32 | { 33 | return [ 34 | new PrivateChannel('User.' . $this->user->id), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Events/CollaborationDeclinedEvent.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function broadcastOn(): array 32 | { 33 | return [ 34 | new PrivateChannel('User.' . $this->user->id), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Events/CollaborationDeletedEvent.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | public function broadcastOn(): array 34 | { 35 | return [ 36 | new PrivateChannel('User.' . $this->user->id), 37 | ]; 38 | } 39 | 40 | /** 41 | * Get the data to broadcast. 42 | * 43 | * @return array 44 | */ 45 | public function broadcastWith(): array 46 | { 47 | return [ 48 | 'vault_id' => $this->vault->id, 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Events/UserNotifiedEvent.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function broadcastOn(): array 32 | { 33 | return [ 34 | new PrivateChannel('User.' . $this->user->id), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Events/VaultFileSystemUpdatedEvent.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function broadcastOn(): array 32 | { 33 | return [ 34 | new PrivateChannel('Vault.' . $this->vault->id), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Events/VaultListUpdatedEvent.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function broadcastOn(): array 32 | { 33 | return [ 34 | new PrivateChannel('User.' . $this->user->id), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Events/VaultNodeDeletedEvent.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function broadcastOn(): array 32 | { 33 | return [ 34 | new PresenceChannel('VaultNode.' . $this->node->id), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Events/VaultNodeUpdatedEvent.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function broadcastOn(): array 32 | { 33 | return [ 34 | new PresenceChannel('VaultNode.' . $this->node->id), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Http/Controllers/FileController.php: -------------------------------------------------------------------------------- 1 | vault); 25 | 26 | if (!$request->has('path')) { 27 | abort(404); 28 | } 29 | 30 | /** @var string $path */ 31 | $path = $request->path; 32 | 33 | if (!str_starts_with($path, '/') && $request->has('node')) { 34 | /** @var VaultNode $node */ 35 | $node = $vault->nodes()->findOrFail($request->node); 36 | 37 | /** 38 | * @var string $currentPath 39 | * 40 | * @phpstan-ignore-next-line larastan.noUnnecessaryCollectionCall 41 | */ 42 | $currentPath = $node->ancestorsAndSelf()->get()->last()->full_path; 43 | $path = new ResolveTwoPaths()->handle($currentPath, $path); 44 | } 45 | 46 | /** @var VaultNode $node */ 47 | $node = new GetVaultNodeFromPath()->handle($vault->id, $path); 48 | $relativePath = new GetPathFromVaultNode()->handle($node); 49 | $absolutePath = Storage::disk('local')->path($relativePath); 50 | 51 | return response()->file($absolutePath); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Livewire/Auth/ForgotPassword.php: -------------------------------------------------------------------------------- 1 | form->sendPasswordResetLink(); 19 | } 20 | 21 | public function render(): Factory|View 22 | { 23 | return view('livewire.auth.forgot-password'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Livewire/Auth/Login.php: -------------------------------------------------------------------------------- 1 | */ 21 | public array $providers; 22 | 23 | public function mount(): void 24 | { 25 | $this->providers = new GetAvailableOAuthProviders()->handle(); 26 | } 27 | 28 | /** 29 | * Handle an incoming authentication request. 30 | */ 31 | public function send(): void 32 | { 33 | $this->form->authenticate(); 34 | 35 | Session::regenerate(); 36 | 37 | /** @var User $user */ 38 | $user = auth()->user(); 39 | $redirectUrl = mb_strlen((string) $user->last_visited_url) > 0 40 | ? $user->last_visited_url 41 | : route('vaults.index', absolute: false); 42 | $this->redirectIntended($redirectUrl); 43 | } 44 | 45 | public function render(): Factory|View 46 | { 47 | return view('livewire.auth.login'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Livewire/Auth/OAuthLogin.php: -------------------------------------------------------------------------------- 1 | redirect(Socialite::driver($provider)->redirect()->getTargetUrl()); 17 | } catch (Exception) { 18 | abort(404); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Livewire/Auth/OAuthLoginCallback.php: -------------------------------------------------------------------------------- 1 | user(); 22 | } catch (Exception) { 23 | session()->flash('error', __('An error occurred while authenticating.')); 24 | $this->redirect('/login'); 25 | 26 | return; 27 | } 28 | 29 | if (!filter_var($providerUser->getEmail(), FILTER_VALIDATE_EMAIL)) { 30 | session()->flash('error', __('No email address found.')); 31 | $this->redirect('/login'); 32 | 33 | return; 34 | } 35 | 36 | $user = User::query()->where('email', $providerUser->getEmail())->first(); 37 | 38 | if (!$user) { 39 | if (!config('settings.registration.enabled')) { 40 | session()->flash('error', __('Registration is currently disabled.')); 41 | $this->redirect('/login'); 42 | 43 | return; 44 | } 45 | 46 | $user = new CreateUser()->handle([ 47 | 'name' => $providerUser->getName() ?? '', 48 | 'email' => $providerUser->getEmail() ?? '', 49 | 'password' => Hash::make(Str::random(32)), 50 | ]); 51 | } 52 | 53 | Auth::login($user, true); 54 | $redirectUrl = mb_strlen((string) $user->last_visited_url) > 0 55 | ? $user->last_visited_url 56 | : route('vaults.index', absolute: false); 57 | $this->redirectIntended($redirectUrl); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Livewire/Auth/Register.php: -------------------------------------------------------------------------------- 1 | form->register(); 19 | 20 | $this->redirect(route('vaults.index', absolute: false)); 21 | } 22 | 23 | public function render(): Factory|View 24 | { 25 | return view('livewire.auth.register'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Livewire/Auth/ResetPassword.php: -------------------------------------------------------------------------------- 1 | form->setToken($token); 22 | $this->form->setEmail(request()->string('email')->toString()); 23 | } 24 | 25 | public function send(): void 26 | { 27 | if (!$this->form->resetPassword()) { 28 | return; 29 | } 30 | 31 | $this->redirect(route('login', absolute: false)); 32 | } 33 | 34 | public function render(): Factory|View 35 | { 36 | return view('livewire.auth.reset-password'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Livewire/Dashboard/Index.php: -------------------------------------------------------------------------------- 1 | user(); 16 | $redirectUrl = mb_strlen((string) $user->last_visited_url) > 0 17 | ? $user->last_visited_url 18 | : route('vaults.index', absolute: false); 19 | $this->redirectIntended($redirectUrl); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Livewire/Forms/CollaborationInviteForm.php: -------------------------------------------------------------------------------- 1 | > 27 | */ 28 | public function rules(): array 29 | { 30 | return [ 31 | 'email' => [ 32 | 'required', 33 | 'string', 34 | 'lowercase', 35 | 'email', 36 | 'max:255', 37 | Rule::exists('users', 'email'), 38 | ], 39 | ]; 40 | } 41 | 42 | public function setVault(Vault $vault): void 43 | { 44 | $this->vault = $vault; 45 | } 46 | 47 | public function create(): void 48 | { 49 | $this->validate(); 50 | 51 | /** @var User $currentUser */ 52 | $currentUser = auth()->user(); 53 | /** @var User $user */ 54 | $user = User::where('email', $this->email)->first(); 55 | 56 | if ($currentUser->id === $user->id) { 57 | throw new Exception(__('You are the owner of this vault.')); 58 | } 59 | 60 | if ($this->vault->collaborators()->wherePivot('user_id', $user->id)->count()) { 61 | throw new Exception(__('This user is already invited.')); 62 | } 63 | 64 | new CreateCollaborationInvite()->handle($this->vault, $user); 65 | $this->reset(['email']); 66 | 67 | $user->notify(new CollaborationInvited($this->vault)); 68 | 69 | broadcast(new UserNotifiedEvent($user)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/Livewire/Forms/EditPasswordForm.php: -------------------------------------------------------------------------------- 1 | > 28 | */ 29 | public function rules(): array 30 | { 31 | return [ 32 | 'current_password' => ['required', 'string', 'current_password'], 33 | 'password' => ['required', 'string', Password::defaults(), 'confirmed'], 34 | ]; 35 | } 36 | 37 | public function update(): void 38 | { 39 | try { 40 | /** @var array $validated */ 41 | $validated = $this->validate(); 42 | } catch (ValidationException $e) { 43 | if (Arr::exists($e->errors(), 'passwordForm.current_password')) { 44 | $this->reset('current_password'); 45 | } 46 | $this->reset('password', 'password_confirmation'); 47 | 48 | throw $e; 49 | } 50 | 51 | $this->reset('current_password', 'password', 'password_confirmation'); 52 | 53 | /** @var User $currentUser */ 54 | $currentUser = auth()->user(); 55 | $currentUser->update([ 56 | 'password' => Hash::make($validated['password']), 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Livewire/Forms/EditProfileForm.php: -------------------------------------------------------------------------------- 1 | > 23 | */ 24 | public function rules(): array 25 | { 26 | return [ 27 | 'name' => [ 28 | 'required', 29 | 'string', 30 | 'max:255', 31 | ], 32 | 'email' => [ 33 | 'required', 34 | 'string', 35 | 'lowercase', 36 | 'email', 37 | 'max:255', 38 | Rule::unique(User::class)->ignore(auth()->user()), 39 | ], 40 | ]; 41 | } 42 | 43 | public function setUser(): void 44 | { 45 | /** @var User $currentUser */ 46 | $currentUser = auth()->user(); 47 | 48 | $this->name = $currentUser->name; 49 | $this->email = $currentUser->email; 50 | } 51 | 52 | public function update(): void 53 | { 54 | /** @var User $currentUser */ 55 | $currentUser = auth()->user(); 56 | 57 | $this->name = mb_trim($this->name); 58 | $this->email = mb_trim($this->email); 59 | 60 | $this->validate(); 61 | 62 | $currentUser->update([ 63 | 'name' => $this->name, 64 | 'email' => $this->email, 65 | ]); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/Livewire/Forms/ForgotPasswordForm.php: -------------------------------------------------------------------------------- 1 | validate(); 22 | 23 | // We will send the password reset link to this user. Once we have attempted 24 | // to send the link, we will examine the response then see the message we 25 | // need to show to the user. Finally, we'll send out a proper response. 26 | /** @var array $credentials */ 27 | $credentials = $this->only('email'); 28 | $status = Password::sendResetLink($credentials); 29 | 30 | if ($status !== Password::RESET_LINK_SENT) { 31 | $this->addError('email', __($status)); 32 | 33 | return; 34 | } 35 | 36 | $this->reset('email'); 37 | 38 | session()->flash('status', __($status)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Livewire/Forms/RegisterForm.php: -------------------------------------------------------------------------------- 1 | > 27 | */ 28 | public function rules(): array 29 | { 30 | return [ 31 | 'name' => ['required', 'string', 'max:255'], 32 | 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class], 33 | 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()], 34 | ]; 35 | } 36 | 37 | /** 38 | * Handle an incoming registration request. 39 | */ 40 | public function register(): void 41 | { 42 | /** @var array $validated */ 43 | $validated = $this->validate(); 44 | $validated['password'] = Hash::make($validated['password']); 45 | event(new Registered($user = new CreateUser()->handle($validated))); 46 | Auth::login($user); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Livewire/Forms/VaultForm.php: -------------------------------------------------------------------------------- 1 | > 26 | */ 27 | public function rules(): array 28 | { 29 | /** @var User $currentUser */ 30 | $currentUser = auth()->user(); 31 | 32 | return [ 33 | 'name' => [ 34 | 'required', 35 | 'min:3', 36 | 'regex:/^[\w]+[\s\w._\-\&\%\#\[\]\(\)]+$/u', 37 | Rule::unique(Vault::class) 38 | ->where('created_by', $currentUser->id) 39 | ->ignore($this->vaultId), 40 | ], 41 | ]; 42 | } 43 | 44 | public function setVault(Vault $vault): void 45 | { 46 | $this->vaultId = $vault->id; 47 | $this->name = $vault->name; 48 | } 49 | 50 | public function create(): void 51 | { 52 | /** @var User $user */ 53 | $user = auth()->user(); 54 | $this->name = mb_trim($this->name); 55 | $this->validate(); 56 | 57 | new CreateVault()->handle($user, [ 58 | 'name' => $this->name, 59 | ]); 60 | $this->reset(['name']); 61 | 62 | broadcast(new VaultListUpdatedEvent($user)); 63 | } 64 | 65 | public function update(): void 66 | { 67 | $vault = Vault::find($this->vaultId); 68 | 69 | if ($vault === null) { 70 | return; 71 | } 72 | 73 | $this->name = mb_trim($this->name); 74 | $this->validate(); 75 | 76 | new UpdateVault()->handle($vault, [ 77 | 'name' => $this->name, 78 | ]); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/Livewire/Layout/UserMenu.php: -------------------------------------------------------------------------------- 1 | profileForm->setUser(); 28 | /** @var array{root: array{pretty_version: string}} $composerInfo */ 29 | $composerInfo = require base_path('vendor/composer/installed.php'); 30 | $this->appVersion = $composerInfo['root']['pretty_version']; 31 | $this->githubUrl = 'https://github.com/brufdev/many-notes'; 32 | } 33 | 34 | public function editProfile(): void 35 | { 36 | $this->profileForm->update(); 37 | 38 | $this->dispatch('close-modal'); 39 | $this->dispatch('toast', message: __('Profile updated'), type: 'success'); 40 | } 41 | 42 | public function editPassword(): void 43 | { 44 | $this->passwordForm->update(); 45 | 46 | $this->dispatch('close-modal'); 47 | $this->dispatch('toast', message: __('Password updated'), type: 'success'); 48 | } 49 | 50 | /** 51 | * Log the current user out of the application. 52 | */ 53 | public function logout(): void 54 | { 55 | Auth::guard('web')->logout(); 56 | 57 | Session::invalidate(); 58 | Session::regenerateToken(); 59 | 60 | $this->redirect(route('login', absolute: false)); 61 | } 62 | 63 | public function render(): Factory|View 64 | { 65 | return view('livewire.layout.userMenu'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/Livewire/Modals/AddNode.php: -------------------------------------------------------------------------------- 1 | authorize('update', $vault); 25 | $this->form->setVault($vault->id); 26 | } 27 | 28 | #[On('open-modal')] 29 | public function open(VaultNode $parent, bool $isFile = true): void 30 | { 31 | if ($parent->vault !== null && $parent->vault->id !== $this->form->vaultId) { 32 | return; 33 | } 34 | 35 | $this->form->parent_id = $parent->id; 36 | $this->form->is_file = $isFile; 37 | $this->form->extension = $isFile ? 'md' : null; 38 | $this->openModal(); 39 | } 40 | 41 | public function add(): void 42 | { 43 | /** @var Vault $vault */ 44 | $vault = Vault::find($this->form->vaultId); 45 | $this->form->create(); 46 | $this->closeModal(); 47 | 48 | $message = $this->form->is_file ? __('File created') : __('Folder created'); 49 | $this->dispatch('toast', message: $message, type: 'success'); 50 | 51 | broadcast(new VaultFileSystemUpdatedEvent($vault)); 52 | } 53 | 54 | public function render(): Factory|View 55 | { 56 | return view('livewire.modals.addNode'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Livewire/Modals/EditNode.php: -------------------------------------------------------------------------------- 1 | authorize('update', $vault); 26 | $this->form->setVault($vault->id); 27 | } 28 | 29 | #[On('open-modal')] 30 | public function open(VaultNode $node): void 31 | { 32 | $this->authorize('update', $node->vault); 33 | $this->form->setNode($node); 34 | $this->openModal(); 35 | } 36 | 37 | public function edit(): void 38 | { 39 | $node = $this->form->update(); 40 | 41 | if (!$node instanceof VaultNode) { 42 | return; 43 | } 44 | 45 | /** @var Vault $vault */ 46 | $vault = $node->vault; 47 | $this->closeModal(); 48 | 49 | $message = $this->form->is_file ? __('File edited') : __('Folder edited'); 50 | $this->dispatch('toast', message: $message, type: 'success'); 51 | 52 | broadcast(new VaultFileSystemUpdatedEvent($vault)); 53 | broadcast(new VaultNodeUpdatedEvent($node)); 54 | } 55 | 56 | public function render(): Factory|View 57 | { 58 | return view('livewire.modals.editNode'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Livewire/Modals/ImportVault.php: -------------------------------------------------------------------------------- 1 | openModal(); 30 | } 31 | 32 | public function updatedFile(): void 33 | { 34 | $this->validate(); 35 | 36 | /** @var User $user */ 37 | $user = auth()->user(); 38 | /** @var TemporaryUploadedFile $file */ 39 | $file = $this->file; 40 | $fileName = $file->getClientOriginalName(); 41 | $filePath = $file->getRealPath(); 42 | 43 | new ProcessImportedVault()->handle($user, $fileName, $filePath); 44 | $this->closeModal(); 45 | 46 | $this->dispatch('toast', message: __('Vault imported'), type: 'success'); 47 | 48 | broadcast(new VaultListUpdatedEvent($user)); 49 | } 50 | 51 | public function render(): Factory|View 52 | { 53 | return view('livewire.modals.importVault'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Livewire/Modals/Modal.php: -------------------------------------------------------------------------------- 1 | show = true; 14 | } 15 | 16 | public function closeModal(): void 17 | { 18 | $this->show = false; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Livewire/Vault/Row.php: -------------------------------------------------------------------------------- 1 | form->setVault($this->vault); 26 | } 27 | 28 | #[Computed] 29 | public function vault(): Vault 30 | { 31 | return Vault::findOrFail($this->vaultId); 32 | } 33 | 34 | public function update(): void 35 | { 36 | $this->authorize('update', $this->vault); 37 | 38 | $this->validate(); 39 | 40 | $this->form->update(); 41 | unset($this->vault); 42 | 43 | $this->dispatch('close-modal'); 44 | $this->dispatch('toast', message: __('Vault edited'), type: 'success'); 45 | 46 | broadcast(new VaultFileSystemUpdatedEvent($this->vault)); 47 | } 48 | 49 | public function render(): Factory|View 50 | { 51 | return view('livewire.vault.row'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Models/Tag.php: -------------------------------------------------------------------------------- 1 | */ 13 | use HasFactory; 14 | } 15 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | */ 16 | use HasFactory; 17 | 18 | use Notifiable; 19 | 20 | /** 21 | * The attributes that should be hidden for serialization. 22 | * 23 | * @var list 24 | */ 25 | protected $hidden = [ 26 | 'password', 27 | 'remember_token', 28 | ]; 29 | 30 | /** 31 | * Get the associated vaults. 32 | * 33 | * @return HasMany 34 | */ 35 | public function vaults(): HasMany 36 | { 37 | return $this->hasMany(Vault::class, 'created_by'); 38 | } 39 | 40 | /** 41 | * Get the collaborations that belongs to the user. 42 | * 43 | * @return BelongsToMany 44 | */ 45 | public function collaborations(): BelongsToMany 46 | { 47 | return $this->belongsToMany(Vault::class)->withPivot('accepted'); 48 | } 49 | 50 | /** 51 | * Get the attributes that should be cast. 52 | * 53 | * @return array 54 | */ 55 | protected function casts(): array 56 | { 57 | return [ 58 | 'email_verified_at' => 'datetime', 59 | 'password' => 'hashed', 60 | ]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Models/Vault.php: -------------------------------------------------------------------------------- 1 | */ 17 | use HasFactory; 18 | 19 | /** 20 | * Get the associated user. 21 | * 22 | * @return BelongsTo 23 | */ 24 | public function user(): BelongsTo 25 | { 26 | return $this->belongsTo(User::class, 'created_by'); 27 | } 28 | 29 | /** 30 | * Get the nodes for the vault. 31 | * 32 | * @return HasMany 33 | */ 34 | public function nodes(): HasMany 35 | { 36 | return $this->hasMany(VaultNode::class); 37 | } 38 | 39 | /** 40 | * Get the associated templates node. 41 | * 42 | * @return HasOne 43 | */ 44 | public function templatesNode(): HasOne 45 | { 46 | return $this->hasOne(VaultNode::class, 'id', 'templates_node_id'); 47 | } 48 | 49 | /** 50 | * Get the collaborators that belongs to the vault. 51 | * 52 | * @return BelongsToMany 53 | */ 54 | public function collaborators(): BelongsToMany 55 | { 56 | return $this->belongsToMany(User::class)->withPivot('accepted'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Notifications/CollaborationAccepted.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function via(object $notifiable): array 32 | { 33 | return ['database']; 34 | } 35 | 36 | /** 37 | * Get the array representation of the notification. 38 | * 39 | * @return array 40 | */ 41 | public function toArray(object $notifiable): array 42 | { 43 | return [ 44 | 'vault_id' => $this->vault->id, 45 | 'user_id' => $this->user->id, 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Notifications/CollaborationDeclined.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function via(object $notifiable): array 32 | { 33 | return ['database']; 34 | } 35 | 36 | /** 37 | * Get the array representation of the notification. 38 | * 39 | * @return array 40 | */ 41 | public function toArray(object $notifiable): array 42 | { 43 | return [ 44 | 'vault_id' => $this->vault->id, 45 | 'user_id' => $this->user->id, 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Notifications/CollaborationInvited.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | public function via(object $notifiable): array 30 | { 31 | return ['database']; 32 | } 33 | 34 | /** 35 | * Get the array representation of the notification. 36 | * 37 | * @return array 38 | */ 39 | public function toArray(object $notifiable): array 40 | { 41 | return [ 42 | 'vault_id' => $this->vault->id, 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Policies/VaultNodePolicy.php: -------------------------------------------------------------------------------- 1 | vault; 20 | 21 | return $user->id === $vault->created_by || 22 | $vault->collaborators() 23 | ->wherePivot('user_id', $user->id) 24 | ->wherePivot('accepted', true) 25 | ->exists(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Policies/VaultPolicy.php: -------------------------------------------------------------------------------- 1 | id === $vault->created_by || 18 | $vault->collaborators() 19 | ->wherePivot('user_id', $user->id) 20 | ->wherePivot('accepted', true) 21 | ->exists(); 22 | } 23 | 24 | /** 25 | * Determine whether the user can update the model. 26 | */ 27 | public function update(User $user, Vault $vault): bool 28 | { 29 | return $user->id === $vault->created_by || 30 | $vault->collaborators() 31 | ->wherePivot('user_id', $user->id) 32 | ->wherePivot('accepted', true) 33 | ->exists(); 34 | } 35 | 36 | /** 37 | * Determine whether the user can delete the model. 38 | */ 39 | public function delete(User $user, Vault $vault): bool 40 | { 41 | return $user->id === $vault->created_by; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Services/VaultFile.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public static function extensions(bool $withDots = false): array 21 | { 22 | return [ 23 | ...Audio::extensions($withDots), 24 | ...Image::extensions($withDots), 25 | ...Note::extensions($withDots), 26 | ...Pdf::extensions($withDots), 27 | ...Video::extensions($withDots), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Services/VaultFiles/Audio.php: -------------------------------------------------------------------------------- 1 | */ 10 | private static array $extensions = [ 11 | 'mp3', 12 | 'flac', 13 | ]; 14 | 15 | /** 16 | * Get the extensions for the audio files. 17 | * 18 | * @return list 19 | */ 20 | public static function extensions(bool $withDots = false): array 21 | { 22 | return $withDots ? File::extensionsWithDots(self::$extensions) : self::$extensions; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Services/VaultFiles/File.php: -------------------------------------------------------------------------------- 1 | $extensions 13 | * @return list 14 | */ 15 | public static function extensionsWithDots(array $extensions): array 16 | { 17 | return array_map(fn (string $value): string => '.' . $value, $extensions); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Services/VaultFiles/Image.php: -------------------------------------------------------------------------------- 1 | */ 10 | private static array $extensions = [ 11 | 'jpg', 12 | 'jpeg', 13 | 'png', 14 | 'gif', 15 | 'webp', 16 | ]; 17 | 18 | /** 19 | * Get the extensions for the image files. 20 | * 21 | * @return list 22 | */ 23 | public static function extensions(bool $withDots = false): array 24 | { 25 | return $withDots ? File::extensionsWithDots(self::$extensions) : self::$extensions; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Services/VaultFiles/Note.php: -------------------------------------------------------------------------------- 1 | */ 10 | private static array $extensions = [ 11 | 'md', 12 | 'txt', 13 | ]; 14 | 15 | /** 16 | * Get the extensions for the note files. 17 | * 18 | * @return list 19 | */ 20 | public static function extensions(bool $withDots = false): array 21 | { 22 | return $withDots ? File::extensionsWithDots(self::$extensions) : self::$extensions; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Services/VaultFiles/Pdf.php: -------------------------------------------------------------------------------- 1 | */ 10 | private static array $extensions = [ 11 | 'pdf', 12 | ]; 13 | 14 | /** 15 | * Get the extensions for the pdf files. 16 | * 17 | * @return list 18 | */ 19 | public static function extensions(bool $withDots = false): array 20 | { 21 | return $withDots ? File::extensionsWithDots(self::$extensions) : self::$extensions; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Services/VaultFiles/Video.php: -------------------------------------------------------------------------------- 1 | */ 10 | private static array $extensions = [ 11 | 'mp4', 12 | 'avi', 13 | ]; 14 | 15 | /** 16 | * Get the extensions for the video files. 17 | * 18 | * @return list 19 | */ 20 | public static function extensions(bool $withDots = false): array 21 | { 22 | return $withDots ? File::extensionsWithDots(self::$extensions) : self::$extensions; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /art/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brufdev/many-notes/4387920b54cc4b4a1fba9898c99d6a064625acbf/art/logo.png -------------------------------------------------------------------------------- /art/theme-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brufdev/many-notes/4387920b54cc4b4a1fba9898c99d6a064625acbf/art/theme-dark.png -------------------------------------------------------------------------------- /art/theme-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brufdev/many-notes/4387920b54cc4b4a1fba9898c99d6a064625acbf/art/theme-light.png -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 17 | 18 | exit($status); 19 | -------------------------------------------------------------------------------- /assets/Starter Vault/00 Resources/Templates/Basic.md: -------------------------------------------------------------------------------- 1 | Date: {{date}} {{time}} 2 | 3 | Tags: 4 | 5 | --- 6 | 7 | {{content}} -------------------------------------------------------------------------------- /assets/Starter Vault/01 Inbox/Features & Tips.md: -------------------------------------------------------------------------------- 1 | Tags: #onboarding 2 | 3 | ## Tips for Effective Note-Taking 4 | 5 | Use **links and tags** to connect related notes. This will help you create a network of interconnected ideas, making it easier to navigate and retrieve information. 6 | 7 | Use **templates** to maintain consistent formatting for your notes. Templates can save you time and ensure that your notes are organized in a uniform manner. 8 | 9 | ## How to Use This App 10 | 11 | Take some time to explore the interface and its features. The user menu, in particular, contains a help section that can guide you through the more complex features. 12 | 13 | --- 14 | 15 | Check out additional information about [Organizational Methods](/01%20Inbox/Organizational%20Methods.md). -------------------------------------------------------------------------------- /assets/Starter Vault/01 Inbox/Next Step.md: -------------------------------------------------------------------------------- 1 | Tags: #onboarding 2 | 3 | ## Congratulations 4 | 5 | Now it's time to **make your own vaults**. 6 | 7 | Keep writing, keep linking, and keep growing. -------------------------------------------------------------------------------- /assets/Starter Vault/01 Inbox/Organizational Methods.md: -------------------------------------------------------------------------------- 1 | Tags: #onboarding 2 | 3 | This folder structure is just one way to organize your notes. Feel free to customize it to fit your needs and use it as inspiration for future vaults. 4 | 5 | ## Why This Structure? 6 | 7 | Each folder has a specific purpose: 8 | 9 | - **00 Resources** - Save reference materials, tutorials, templates, and useful links 10 | - **01 Inbox** - Store incoming notes, ideas, tasks, and reminders for later review 11 | - **02 Personal** - Compile notes on personal development, wellness, hobbies, and life events 12 | - **03 Professional** - Document work-related notes, meetings, career goals, and evaluations 13 | - **04 Projects** - Keep detailed notes on specific projects, including plans and timelines 14 | - **05 Interests** - Collect notes on topics you love, such as articles and research ideas 15 | 16 | ## Organizational Methods 17 | 18 | You can also explore popular note-taking methods like: 19 | 20 | - **Zettelkasten** - Atomic, linked ideas 21 | - **PARA Method** - Projects, Areas, Resources, Archive 22 | - **Johnny Decimal System** - Strict folder numbering for categories 23 | 24 | The key is to choose a system that works best for your mind. 25 | 26 | --- 27 | 28 | What's [next](/01%20Inbox/Next%20Step.md)? -------------------------------------------------------------------------------- /assets/Starter Vault/02 Personal/Goals.md: -------------------------------------------------------------------------------- 1 | Tags: #goals #selfgrowth 2 | 3 | ## Goals for this year 4 | 5 | - [x] Install Many Notes 6 | - [ ] Start writing daily -------------------------------------------------------------------------------- /assets/Starter Vault/04 Projects/04.01 Example Project/Project Ideas.md: -------------------------------------------------------------------------------- 1 | # Example Project: Learn Markdown 2 | 3 | Status: In Progress 4 | 5 | Tags: #markdown 6 | 7 | ## Notes 8 | 9 | - Markdown is a lightweight markup language. 10 | - It supports **bold**, *italic*, and `inline code`. 11 | 12 | ## Next Steps 13 | 14 | - Practice writing Markdown 15 | -------------------------------------------------------------------------------- /assets/Starter Vault/Start Here.md: -------------------------------------------------------------------------------- 1 | # Welcome to your **Starter Vault** 2 | 3 | Tags: #onboarding 4 | 5 | This app is your second brain. A place where your thoughts, projects, and ideas come to life. 6 | 7 | --- 8 | 9 | Follow the links to see how notes can connect. First, let's explore the [Features & Tips](/01%20Inbox/Features%20&%20Tips.md). -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 11 | web: __DIR__ . '/../routes/web.php', 12 | commands: __DIR__ . '/../routes/console.php', 13 | channels: __DIR__ . '/../routes/channels.php', 14 | health: '/up', 15 | ) 16 | ->withMiddleware(function (Middleware $middleware): void { 17 | $middleware->trustProxies(at: '*'); 18 | }) 19 | ->withExceptions(function (Exceptions $exceptions): void { 20 | // 21 | })->create(); 22 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'enabled' => env('SETTINGS_REGISTRATION_ENABLED', true), 18 | ], 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/TagFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class TagFactory extends Factory 13 | { 14 | /** 15 | * Define the model's default state. 16 | * 17 | * @return array 18 | */ 19 | public function definition(): array 20 | { 21 | return [ 22 | 'name' => fake()->unique()->words(1, true), 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class UserFactory extends Factory 15 | { 16 | /** 17 | * The current password being used by the factory. 18 | */ 19 | private static ?string $password = null; 20 | 21 | /** 22 | * Define the model's default state. 23 | * 24 | * @return array 25 | */ 26 | public function definition(): array 27 | { 28 | return [ 29 | 'name' => fake()->name(), 30 | 'email' => fake()->unique()->safeEmail(), 31 | 'email_verified_at' => now(), 32 | 'password' => self::$password ??= Hash::make('password'), 33 | 'remember_token' => Str::random(10), 34 | 'last_visited_url' => null, 35 | ]; 36 | } 37 | 38 | /** 39 | * Indicate that the model's email address should be unverified. 40 | */ 41 | public function unverified(): static 42 | { 43 | return $this->state(fn (array $attributes): array => [ 44 | 'email_verified_at' => null, 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /database/factories/VaultFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class VaultFactory extends Factory 14 | { 15 | /** 16 | * Define the model's default state. 17 | * 18 | * @return array 19 | */ 20 | public function definition(): array 21 | { 22 | return [ 23 | 'name' => fake()->words(3, true), 24 | 'created_by' => User::factory(), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/factories/VaultNodeFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class VaultNodeFactory extends Factory 14 | { 15 | /** 16 | * Define the model's default state. 17 | * 18 | * @return array 19 | */ 20 | public function definition(): array 21 | { 22 | return [ 23 | 'vault_id' => Vault::factory(), 24 | 'parent_id' => null, 25 | 'is_file' => true, 26 | 'name' => fake()->words(3, true), 27 | 'extension' => 'md', 28 | 'content' => fake()->paragraph(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->timestamp('email_verified_at')->nullable(); 21 | $table->string('password'); 22 | $table->rememberToken(); 23 | $table->timestamps(); 24 | }); 25 | 26 | Schema::create('password_reset_tokens', function (Blueprint $table): void { 27 | $table->string('email')->primary(); 28 | $table->string('token'); 29 | $table->timestamp('created_at')->nullable(); 30 | }); 31 | 32 | Schema::create('sessions', function (Blueprint $table): void { 33 | $table->string('id')->primary(); 34 | $table->foreignId('user_id')->nullable()->index(); 35 | $table->string('ip_address', 45)->nullable(); 36 | $table->text('user_agent')->nullable(); 37 | $table->longText('payload'); 38 | $table->integer('last_activity')->index(); 39 | }); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 18 | $table->mediumText('value'); 19 | $table->integer('expiration'); 20 | }); 21 | 22 | Schema::create('cache_locks', function (Blueprint $table): void { 23 | $table->string('key')->primary(); 24 | $table->string('owner'); 25 | $table->integer('expiration'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('queue')->index(); 19 | $table->longText('payload'); 20 | $table->unsignedTinyInteger('attempts'); 21 | $table->unsignedInteger('reserved_at')->nullable(); 22 | $table->unsignedInteger('available_at'); 23 | $table->unsignedInteger('created_at'); 24 | }); 25 | 26 | Schema::create('job_batches', function (Blueprint $table): void { 27 | $table->string('id')->primary(); 28 | $table->string('name'); 29 | $table->integer('total_jobs'); 30 | $table->integer('pending_jobs'); 31 | $table->integer('failed_jobs'); 32 | $table->longText('failed_job_ids'); 33 | $table->mediumText('options')->nullable(); 34 | $table->integer('cancelled_at')->nullable(); 35 | $table->integer('created_at'); 36 | $table->integer('finished_at')->nullable(); 37 | }); 38 | 39 | Schema::create('failed_jobs', function (Blueprint $table): void { 40 | $table->id(); 41 | $table->string('uuid')->unique(); 42 | $table->text('connection'); 43 | $table->text('queue'); 44 | $table->longText('payload'); 45 | $table->longText('exception'); 46 | $table->timestamp('failed_at')->useCurrent(); 47 | }); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /database/migrations/2024_07_16_025200_create_vaults_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->foreignId('created_by')->constrained('users'); 20 | $table->timestamp('opened_at')->nullable(); 21 | $table->timestamps(); 22 | $table->softDeletes(); 23 | }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /database/migrations/2024_08_16_010335_create_vault_nodes_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('vault_id')->constrained('vaults'); 19 | $table->foreignId('parent_id')->nullable()->constrained('vault_nodes'); 20 | $table->unsignedTinyInteger('is_file'); 21 | $table->string('name'); 22 | $table->string('extension')->nullable(); 23 | $table->mediumText('content')->nullable(); 24 | $table->timestamps(); 25 | $table->softDeletes(); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2024_12_17_132832_add_templates_node_id_to_vaults_table.php: -------------------------------------------------------------------------------- 1 | foreignId('templates_node_id')->nullable()->constrained('vault_nodes')->nullOnDelete(); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_12_23_150046_remove_soft_deletes_from_vaults.php: -------------------------------------------------------------------------------- 1 | dropSoftDeletes(); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_12_23_151515_remove_soft_deletes_from_vault_nodes.php: -------------------------------------------------------------------------------- 1 | dropSoftDeletes(); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2025_02_06_131404_create_vault_node_vault_node_table.php: -------------------------------------------------------------------------------- 1 | foreignId('source_id')->constrained('vault_nodes'); 18 | $table->foreignId('destination_id')->constrained('vault_nodes'); 19 | $table->unsignedMediumInteger('position'); 20 | 21 | $table->primary(['source_id', 'destination_id', 'position']); 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations/2025_02_08_192143_upgrades.php: -------------------------------------------------------------------------------- 1 | unsignedTinyInteger('executed'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2025_02_10_190148_create_tags_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /database/migrations/2025_02_10_190644_create_tag_vault_node_table.php: -------------------------------------------------------------------------------- 1 | foreignId('vault_node_id')->constrained('vault_nodes'); 18 | $table->foreignId('tag_id')->constrained('tags'); 19 | $table->unsignedMediumInteger('position'); 20 | 21 | $table->primary(['tag_id', 'vault_node_id', 'position']); 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations/2025_03_13_200017_create_notifications_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 18 | $table->string('type'); 19 | $table->morphs('notifiable'); 20 | $table->text('data'); 21 | $table->timestamp('read_at')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /database/migrations/2025_03_22_152303_create_user_vault_table.php: -------------------------------------------------------------------------------- 1 | foreignId('user_id')->constrained('users'); 18 | $table->foreignId('vault_id')->constrained('vaults'); 19 | $table->unsignedTinyInteger('accepted'); 20 | 21 | $table->primary(['user_id', 'vault_id', 'accepted']); 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations/2025_03_31_181055_remove_opened_at_field_from_vaults_table.php: -------------------------------------------------------------------------------- 1 | dropColumn('opened_at'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2025_03_31_181654_add_last_visited_url_to_users_table.php: -------------------------------------------------------------------------------- 1 | string('last_visited_url')->nullable(); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 19 | 20 | User::factory()->create([ 21 | 'name' => 'Test User', 22 | 'email' => 'test@example.com', 23 | ]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /deploy/docker/entrypoint/75-upgrades.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | php "$APP_BASE_DIR/artisan" upgrade:run 3 | -------------------------------------------------------------------------------- /deploy/docker/nginx/reverb.conf: -------------------------------------------------------------------------------- 1 | # Reverb WebSocket 2 | location /ws { 3 | # Strip the /ws prefix when proxying to the Reverb server 4 | rewrite ^/ws(.*)$ $1 break; 5 | 6 | proxy_pass http://127.0.0.1:6001; 7 | proxy_http_version 1.1; 8 | proxy_set_header Upgrade $http_upgrade; 9 | proxy_set_header Connection "upgrade"; 10 | } 11 | -------------------------------------------------------------------------------- /deploy/docker/s6-overlay/reverb/dependencies: -------------------------------------------------------------------------------- 1 | nginx -------------------------------------------------------------------------------- /deploy/docker/s6-overlay/reverb/finish: -------------------------------------------------------------------------------- 1 | #!/command/execlineb -P 2 | echo "Reverb process exited with status $1" -------------------------------------------------------------------------------- /deploy/docker/s6-overlay/reverb/run: -------------------------------------------------------------------------------- 1 | #!/command/execlineb -P 2 | with-contenv 3 | /usr/local/bin/php /var/www/html/artisan reverb:start --host=0.0.0.0 --port=6001 -------------------------------------------------------------------------------- /deploy/docker/s6-overlay/reverb/type: -------------------------------------------------------------------------------- 1 | longrun -------------------------------------------------------------------------------- /deploy/docker/s6-overlay/typesense/dependencies: -------------------------------------------------------------------------------- 1 | nginx -------------------------------------------------------------------------------- /deploy/docker/s6-overlay/typesense/finish: -------------------------------------------------------------------------------- 1 | #!/command/execlineb -P 2 | echo "Typesense process exited with status $1" -------------------------------------------------------------------------------- /deploy/docker/s6-overlay/typesense/run: -------------------------------------------------------------------------------- 1 | #!/command/execlineb -P 2 | with-contenv 3 | /opt/typesense-server --data-dir=/var/www/html/typesense --api-key=xyz -------------------------------------------------------------------------------- /deploy/docker/s6-overlay/typesense/type: -------------------------------------------------------------------------------- 1 | longrun -------------------------------------------------------------------------------- /deploy/docker/s6-overlay/user/reverb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brufdev/many-notes/4387920b54cc4b4a1fba9898c99d6a064625acbf/deploy/docker/s6-overlay/user/reverb -------------------------------------------------------------------------------- /deploy/docker/s6-overlay/user/typesense: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brufdev/many-notes/4387920b54cc4b4a1fba9898c99d6a064625acbf/deploy/docker/s6-overlay/user/typesense -------------------------------------------------------------------------------- /docs/customization/oauth.md: -------------------------------------------------------------------------------- 1 | ## Authelia 2 | 3 | To enable Authelia OAuth, add: 4 | 5 | ```yaml 6 | environment: 7 | - AUTHELIA_CLIENT_ID=CLIENT_ID # change id 8 | - AUTHELIA_CLIENT_SECRET=CLIENT_SECRET # change secret 9 | - AUTHELIA_REDIRECT_URI=http://localhost/oauth/authelia/callback # change domain 10 | - AUTHELIA_BASE_URL=http://your-authelia-url # change url 11 | ``` 12 | 13 | ## Authentik 14 | 15 | To enable Authentik OAuth, add: 16 | 17 | ```yaml 18 | environment: 19 | - AUTHENTIK_CLIENT_ID=CLIENT_ID # change id 20 | - AUTHENTIK_CLIENT_SECRET=CLIENT_SECRET # change secret 21 | - AUTHENTIK_REDIRECT_URI=http://localhost/oauth/authentik/callback # change domain 22 | - AUTHENTIK_BASE_URL=http://your-authentik-url # change url 23 | ``` 24 | 25 | ## Keycloak 26 | 27 | To enable Keycloak OAuth, add: 28 | 29 | ```yaml 30 | environment: 31 | - KEYCLOAK_CLIENT_ID=CLIENT_ID # change id 32 | - KEYCLOAK_CLIENT_SECRET=CLIENT_SECRET # change secret 33 | - KEYCLOAK_REDIRECT_URI=http://localhost/oauth/keycloak/callback # change domain 34 | - KEYCLOAK_BASE_URL=http://your-keycloak-url # change url 35 | - KEYCLOAK_REALM=YOUR_REALM # change realm 36 | ``` 37 | 38 | ## Zitadel 39 | 40 | To enable Zitadel OAuth, add: 41 | 42 | ```yaml 43 | environment: 44 | - ZITADEL_CLIENT_ID=CLIENT_ID # change id 45 | - ZITADEL_CLIENT_SECRET=CLIENT_SECRET # change secret 46 | - ZITADEL_REDIRECT_URI=http://localhost/oauth/zitadel/callback # change domain 47 | - ZITADEL_BASE_URL=http://your-zitadel-url # change url 48 | - ZITADEL_ORGANIZATION_ID=ORGANIZATION_ID # change id (optional configuration) 49 | - ZITADEL_PROJECT_ID=PROJECT_ID # change id (optional configuration) 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/installation/docker-bind-mounts.md: -------------------------------------------------------------------------------- 1 | # Installation guide (Docker with bind mounts) 2 | 3 | **Read the [upgrading guide](../../UPGRADING.md) if you are upgrading from a previous version.** 4 | 5 | Many Notes must have the necessary permissions to access the shared paths. Since this image runs with an unprivileged user, the host user IDs must be added during the build phase. 6 | 7 | ## Instructions 8 | 9 | First, create a new directory called `many-notes` with the following structure: 10 | 11 | ``` 12 | many-notes/ 13 | ├── database/ 14 | ├── logs/ 15 | ├── private/ 16 | ├── typesense/ 17 | ``` 18 | 19 | Next, create a `Dockerfile` file with: 20 | 21 | ```Dockerfile 22 | FROM brufdev/many-notes:latest 23 | USER root 24 | ARG UID 25 | ARG GID 26 | RUN docker-php-serversideup-set-id www-data $UID:$GID && \ 27 | docker-php-serversideup-set-file-permissions --owner $UID:$GID --service nginx 28 | USER www-data 29 | ``` 30 | 31 | Finally, create a `compose.yaml` file with: 32 | 33 | ```yaml 34 | services: 35 | php: 36 | build: 37 | context: . 38 | args: 39 | UID: USER_ID # change id 40 | GID: GROUP_ID # change id 41 | restart: unless-stopped 42 | environment: 43 | - APP_URL=http://localhost # change url 44 | volumes: 45 | - ./database:/var/www/html/database/sqlite 46 | - ./logs:/var/www/html/storage/logs 47 | - ./private:/var/www/html/storage/app/private 48 | - ./typesense:/var/www/html/typesense 49 | ports: 50 | - 80:8080 51 | ``` 52 | 53 | Make sure to update the IDs to match the host user IDs. Feel free to change anything else if you know what you're doing, and read the [customization section](../../README.md#customization) before continuing. Then run: 54 | 55 | ```shell 56 | docker compose up -d 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/installation/docker-different-database.md: -------------------------------------------------------------------------------- 1 | # Installation guide (Docker with a different database) 2 | 3 | **Read the [upgrading guide](../../UPGRADING.md) if you are upgrading from a previous version.** 4 | 5 | Many Notes uses SQLite by default but supports other databases like MariaDB, MySQL, and PostgreSQL. This guide will use MariaDB, but you can easily adapt it to one of the other databases. 6 | 7 | ## Instructions 8 | 9 | Create a `compose.yaml` file with: 10 | 11 | ```yaml 12 | services: 13 | php: 14 | image: brufdev/many-notes:latest 15 | restart: unless-stopped 16 | environment: 17 | - APP_URL=http://localhost # change url 18 | - DB_CONNECTION=mariadb 19 | - DB_HOST=many-notes-mariadb-1 20 | - DB_PORT=3306 21 | - DB_DATABASE=manynotes 22 | - DB_USERNAME=user 23 | - DB_PASSWORD=USER_PASSWORD # change password 24 | volumes: 25 | - logs:/var/www/html/storage/logs 26 | - private:/var/www/html/storage/app/private 27 | - typesense:/var/www/html/typesense 28 | ports: 29 | - 80:8080 30 | mariadb: 31 | image: mariadb:11 32 | restart: unless-stopped 33 | environment: 34 | - MARIADB_ROOT_PASSWORD=ROOT_PASSWORD # change password 35 | - MARIADB_DATABASE=manynotes 36 | - MARIADB_USER=user 37 | - MARIADB_PASSWORD=USER_PASSWORD # change password 38 | volumes: 39 | - database:/var/lib/mysql 40 | 41 | volumes: 42 | database: 43 | logs: 44 | private: 45 | typesense: 46 | ``` 47 | 48 | Make sure to change the passwords. Feel free to change anything else if you know what you're doing, and read the [customization section](../../README.md#customization) before continuing. Then run: 49 | 50 | ```shell 51 | docker compose up -d 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/installation/non-docker.md: -------------------------------------------------------------------------------- 1 | # Installation guide (non-Docker) 2 | 3 | **Read the [upgrading guide](../../UPGRADING.md) if you are upgrading from a previous version.** 4 | 5 | The Docker method is recommended because it is faster and simpler to set up. However, if you prefer a non-Docker installation, here are the full instructions. 6 | 7 | ## Requirements 8 | 9 | PHP 8.4+, Composer, npm and Git 10 | 11 | ## Instructions 12 | 13 | Clone the project: 14 | 15 | ```shell 16 | git clone https://github.com/brufdev/many-notes.git 17 | ``` 18 | 19 | Install Composer dependencies 20 | 21 | ```shell 22 | composer install --no-dev --optimize-autoloader 23 | ``` 24 | 25 | Install npm dependencies 26 | 27 | ```shell 28 | npm install 29 | ``` 30 | 31 | Run the npm build 32 | 33 | ```shell 34 | npm run build 35 | ``` 36 | 37 | Create the SQLite database 38 | 39 | ```shell 40 | touch database/sqlite/database.sqlite 41 | ``` 42 | 43 | Create .env file 44 | 45 | ```shell 46 | cp .env.example .env 47 | ``` 48 | 49 | Generate application key 50 | 51 | ```shell 52 | php artisan key:generate 53 | ``` 54 | 55 | Create caches to optimize the application 56 | 57 | ```shell 58 | php artisan optimize 59 | ``` 60 | 61 | Create the symbolic link for Many Notes public storage 62 | 63 | ```shell 64 | php artisan storage:link 65 | ``` 66 | 67 | Run the database migrations 68 | 69 | ```shell 70 | php artisan migrate 71 | ``` 72 | 73 | Run the upgrade command 74 | 75 | ```shell 76 | php artisan upgrade:run 77 | ``` 78 | 79 | The way to customize Many Notes in a non-Docker installation is to add/update the variables in the `.env` file at the root of the project. The only exception is customizing the upload size limit, which needs to be changed in your PHP settings. Read the [customization section](../../README.md#customization) before continuing. 80 | -------------------------------------------------------------------------------- /docs/support/faqs.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | This guide will help you find the answers to the most common questions about Many Notes. 4 | 5 |
6 | How to debug the application 7 |
8 | 9 | You can enable debug mode in your `compose.yaml` file by adding: 10 | 11 | ```yaml 12 | environment: 13 | - APP_DEBUG=true 14 | ``` 15 | 16 | You can also enable Typesense debug mode in your `compose.yaml` file by adding: 17 | 18 | ```yaml 19 | environment: 20 | - GLOG_minloglevel=2 21 | ``` 22 | 23 | The numbers of severity levels `INFO`, `WARNING`, `ERROR`, and `FATAL` are 0, 1, 2, and 3, respectively. 24 |
25 |
26 | 27 |
28 | Why is the build phase required when using bind mounts 29 |
30 | 31 | The build phase may seem unnecessary when using bind mounts, but since the Docker image runs with an unprivileged user, updating permissions for files and services can only be done during the build stage. I have created a [discussion](https://github.com/brufdev/many-notes/discussions/40) to share my perspective on this topic. Feel free to join and share your thoughts. 32 |
33 |
34 | 35 |
36 | The search feature is not returning any results 37 |
38 | 39 | First, make sure to mount the Typesense directory to `/var/www/html/typesense`, like is described in the [installation guide](../../README.md#installation). 40 | 41 | After that, you need to reimport the existing data into Typesense by simply running the following command on a container shell: 42 | 43 | ```shell 44 | php artisan upgrade:reimport-data-into-typesense 45 | ``` 46 |
47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "vite build", 6 | "dev": "vite" 7 | }, 8 | "devDependencies": { 9 | "@tailwindcss/forms": "^0.5.10", 10 | "@tailwindcss/vite": "^4.1.5", 11 | "autoprefixer": "^10.4.21", 12 | "axios": "^1.9.0", 13 | "laravel-echo": "^2.0.2", 14 | "laravel-vite-plugin": "^1.2.0", 15 | "pusher-js": "^8.4.0", 16 | "tailwindcss": "^4.1.5", 17 | "vite": "^6.3.4" 18 | }, 19 | "dependencies": { 20 | "dompurify": "^3.2.5", 21 | "marked": "^15.0.11" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/larastan/larastan/extension.neon 3 | - vendor/calebdw/larastan-livewire/extension.neon 4 | - vendor/nesbot/carbon/extension.neon 5 | 6 | parameters: 7 | paths: 8 | - app/ 9 | level: 10 10 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/assets/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brufdev/many-notes/4387920b54cc4b4a1fba9898c99d6a064625acbf/public/assets/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/assets/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brufdev/many-notes/4387920b54cc4b4a1fba9898c99d6a064625acbf/public/assets/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brufdev/many-notes/4387920b54cc4b4a1fba9898c99d6a064625acbf/public/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /public/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brufdev/many-notes/4387920b54cc4b4a1fba9898c99d6a064625acbf/public/assets/favicon-16x16.png -------------------------------------------------------------------------------- /public/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brufdev/many-notes/4387920b54cc4b4a1fba9898c99d6a064625acbf/public/assets/favicon-32x32.png -------------------------------------------------------------------------------- /public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brufdev/many-notes/4387920b54cc4b4a1fba9898c99d6a064625acbf/public/assets/favicon.ico -------------------------------------------------------------------------------- /public/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brufdev/many-notes/4387920b54cc4b4a1fba9898c99d6a064625acbf/public/assets/logo.png -------------------------------------------------------------------------------- /public/assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Many Notes", 3 | "short_name": "Many Notes", 4 | "icons": [ 5 | { 6 | "src": "/assets/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/assets/android-chrome-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 20 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 9 | __DIR__ . '/app', 10 | __DIR__ . '/bootstrap/app.php', 11 | __DIR__ . '/bootstrap/providers.php', 12 | __DIR__ . '/database', 13 | __DIR__ . '/public', 14 | __DIR__ . '/routes', 15 | __DIR__ . '/tests', 16 | ]) 17 | ->withPreparedSets( 18 | deadCode: true, 19 | codeQuality: true, 20 | typeDeclarations: true, 21 | privatization: true, 22 | earlyReturn: true, 23 | strictBooleans: true, 24 | ) 25 | ->withPhpSets(); 26 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import './bootstrap'; 2 | import { marked } from 'marked'; 3 | import DOMPurify from 'dompurify'; 4 | 5 | window.marked = marked; 6 | window.DOMPurify = DOMPurify; 7 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import './echo'; 3 | 4 | window.axios = axios; 5 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 6 | -------------------------------------------------------------------------------- /resources/js/echo.js: -------------------------------------------------------------------------------- 1 | import Echo from 'laravel-echo'; 2 | import Pusher from 'pusher-js'; 3 | 4 | window.Pusher = Pusher; 5 | window.Echo = new Echo({ 6 | broadcaster: 'reverb', 7 | key: import.meta.env.VITE_REVERB_APP_KEY, 8 | wsHost: window.location.hostname, 9 | wsPort: window.location.port !== '' ? window.location.port : 80, 10 | wsPath: '/ws', 11 | forceTLS: false, 12 | enabledTransports: ['ws'], 13 | }); 14 | -------------------------------------------------------------------------------- /resources/views/components/form/button.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'primary' => false, 3 | ]) 4 | 5 | 15 | -------------------------------------------------------------------------------- /resources/views/components/form/checkbox.blade.php: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /resources/views/components/form/index.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{ $slot }} 3 |
4 | -------------------------------------------------------------------------------- /resources/views/components/form/input.blade.php: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /resources/views/components/form/link.blade.php: -------------------------------------------------------------------------------- 1 | 5 | {{ $slot }} 6 | 7 | -------------------------------------------------------------------------------- /resources/views/components/form/linkButton.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'primary' => false, 3 | 'full' => false, 4 | ]) 5 | 6 | $primary, 11 | 'border-light-base-400 dark:border-base-700 bg-light-base-300 dark:bg-base-500 hover:bg-light-base-400 dark:hover:bg-base-700 text-light-base-950 dark:text-base-50' => !$primary, 12 | 'w-full justify-center' => $full, 13 | ]) 14 | > 15 | {{ $slot }} 16 | 17 | -------------------------------------------------------------------------------- /resources/views/components/form/sessionError.blade.php: -------------------------------------------------------------------------------- 1 | @props(['error']) 2 | 3 | @if ($error) 4 |

merge(['class' => 'font-medium text-sm text-error-600 dark:text-error-500']) }}> 5 | {{ $error }} 6 |

7 | @endif 8 | -------------------------------------------------------------------------------- /resources/views/components/form/sessionStatus.blade.php: -------------------------------------------------------------------------------- 1 | @props(['status']) 2 | 3 | @if ($status) 4 |

merge(['class' => 'font-medium text-sm text-success-600 dark:text-success-500']) }}> 5 | {{ $status }} 6 |

7 | @endif 8 | -------------------------------------------------------------------------------- /resources/views/components/form/submit.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'label', 3 | 'target', 4 | ]) 5 | 6 | 18 | -------------------------------------------------------------------------------- /resources/views/components/form/text.blade.php: -------------------------------------------------------------------------------- 1 |

2 | {{ $slot }} 3 |

4 | -------------------------------------------------------------------------------- /resources/views/components/icons/arrowDownOnSquare.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/arrowDownTray.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/arrowRightEndOnRectangle.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/arrowRightStartOnRectangle.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/arrowUpOnSquare.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/arrowUpTray.blade.php: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/bars3.blade.php: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /resources/views/components/icons/bars3BottomLeft.blade.php: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /resources/views/components/icons/bars3BottomRight.blade.php: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /resources/views/components/icons/bell.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/bookOpen.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/checkCircle.blade.php: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/chevronDown.blade.php: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /resources/views/components/icons/chevronRight.blade.php: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /resources/views/components/icons/circleStack.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/codeBracket.blade.php: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/documentDuplicate.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/documentPlus.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/ellipsisVertical.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/exclamationCircle.blade.php: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/exclamationTriangle.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/folder.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/folderPlus.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/informationCircle.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/lockClosed.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/magnifyingGlass.blade.php: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/pencilSquare.blade.php: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/plus.blade.php: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /resources/views/components/icons/questionMarkCircle.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/spinner.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/trash.blade.php: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/user.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/userGroup.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/xMark.blade.php: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /resources/views/components/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $title ?? 'Many Notes' }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @vite('resources/css/app.css') 17 | @vite('resources/js/app.js') 18 | 19 | 20 | 21 | {{ $slot }} 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /resources/views/components/layouts/appHeader.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ $slot }} 5 |
6 |
7 |
8 | -------------------------------------------------------------------------------- /resources/views/components/layouts/appMain.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ $slot }} 4 | 5 | 6 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /resources/views/components/layouts/guestMain.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | Many Notes 4 |
5 | 6 |
7 |
8 | {{ $slot }} 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /resources/views/components/markdownEditor/button.blade.php: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/views/components/markdownEditor/itemDivider.blade.php: -------------------------------------------------------------------------------- 1 |
  • 2 | -------------------------------------------------------------------------------- /resources/views/components/markdownEditor/itemDropdown.blade.php: -------------------------------------------------------------------------------- 1 |
  • 7 | {{ $slot }} 8 |
  • 9 | -------------------------------------------------------------------------------- /resources/views/components/markdownEditor/items.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
      3 | {{ $slot }} 4 |
    5 |
    6 | -------------------------------------------------------------------------------- /resources/views/components/markdownEditor/subButton.blade.php: -------------------------------------------------------------------------------- 1 |
  • 2 | 9 |
  • 10 | -------------------------------------------------------------------------------- /resources/views/components/menu/button.blade.php: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /resources/views/components/menu/close.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | {{ $slot }} 3 |
    4 | -------------------------------------------------------------------------------- /resources/views/components/menu/index.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'wide' => false, 3 | 'anchorElement' => null, 4 | 'anchorOffset' => null, 5 | ]) 6 | 7 |
    merge(['class' => 'relative']) }} 9 | x-data="{ 10 | menuOpen: false, 11 | wide: {{ $wide ? 'true' : 'false' }}, 12 | }" 13 | > 14 | {{ $slot }} 15 |
    16 | -------------------------------------------------------------------------------- /resources/views/components/menu/item.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 8 |
    9 | -------------------------------------------------------------------------------- /resources/views/components/menu/itemDivider.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | -------------------------------------------------------------------------------- /resources/views/components/menu/itemLink.blade.php: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /resources/views/components/menu/items.blade.php: -------------------------------------------------------------------------------- 1 | @aware([ 2 | 'anchorElement', 3 | 'anchorOffset', 4 | ]) 5 | 6 |
    18 | {{ $slot }} 19 |
    20 | -------------------------------------------------------------------------------- /resources/views/components/modal/close.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | {{ $slot }} 3 |
    4 | -------------------------------------------------------------------------------- /resources/views/components/modal/index.blade.php: -------------------------------------------------------------------------------- 1 |
    8 | {{ $slot }} 9 |
    10 | -------------------------------------------------------------------------------- /resources/views/components/modal/open.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | {{ $slot }} 3 |
    4 | -------------------------------------------------------------------------------- /resources/views/components/modal/panel.blade.php: -------------------------------------------------------------------------------- 1 | @props(['top' => false]) 2 | 3 | @aware(['title']) 4 | 5 | 33 | -------------------------------------------------------------------------------- /resources/views/components/notification/collaborationAccepted.blade.php: -------------------------------------------------------------------------------- 1 | @props(['notification']) 2 | 3 | 4 | {{ $notification['message'] }} 5 | 6 | -------------------------------------------------------------------------------- /resources/views/components/notification/collaborationDeclined.blade.php: -------------------------------------------------------------------------------- 1 | @props(['notification']) 2 | 3 | 4 | {{ $notification['message'] }} 5 | 6 | -------------------------------------------------------------------------------- /resources/views/components/notification/collaborationInvited.blade.php: -------------------------------------------------------------------------------- 1 | @props(['notification']) 2 | 3 | 4 | {{ $notification['message'] }} 5 | 6 | -------------------------------------------------------------------------------- /resources/views/components/toast/index.blade.php: -------------------------------------------------------------------------------- 1 |
    19 |
      20 | 32 |
    33 |
    34 | -------------------------------------------------------------------------------- /resources/views/components/treeView/badge.blade.php: -------------------------------------------------------------------------------- 1 | 5 | {{ $slot }} 6 | 7 | -------------------------------------------------------------------------------- /resources/views/components/treeView/index.blade.php: -------------------------------------------------------------------------------- 1 |
    8 | {{ $slot }} 9 |
    10 | 11 | @script 12 | 49 | @endscript 50 | -------------------------------------------------------------------------------- /resources/views/components/treeView/item.blade.php: -------------------------------------------------------------------------------- 1 | @props(['node']) 2 | 3 |
  • 4 | {{ $slot }} 5 |
  • 6 | -------------------------------------------------------------------------------- /resources/views/components/treeView/itemFile.blade.php: -------------------------------------------------------------------------------- 1 | @aware(['node']) 2 | 3 |
    4 | 5 | 15 | 16 | {{ $node->name }} 17 | 18 | 19 | @if (!in_array($node->extension, App\Services\VaultFiles\Note::extensions())) 20 | {{ $node->extension }} 21 | @endif 22 | 23 | 24 | 25 | 26 | 27 | 28 | {{ __('Rename') }} 29 | 30 | 31 | 32 | 33 | {{ __('Move') }} 34 | 35 | 36 | 40 | 41 | {{ __('Delete') }} 42 | 43 | 44 | 45 | 46 |
    47 | -------------------------------------------------------------------------------- /resources/views/components/treeView/items.blade.php: -------------------------------------------------------------------------------- 1 | @props(['root']) 2 | 3 |
      11 | {{ $slot }} 12 |
    13 | -------------------------------------------------------------------------------- /resources/views/components/vault/treeViewNode.blade.php: -------------------------------------------------------------------------------- 1 | @props(['nodes', 'root']) 2 | 3 | 4 | @foreach ($nodes as $node) 5 | 6 | @endforeach 7 | 8 | -------------------------------------------------------------------------------- /resources/views/components/vault/treeViewRow.blade.php: -------------------------------------------------------------------------------- 1 | @props(['node']) 2 | 3 | 4 | @if (!$node->is_file) 5 | 6 | 7 | @if (!empty($node->children) && $node->children->count()) 8 | @include('components.vault.treeViewNode', ['nodes' => $node->children, 'root' => false]) 9 | @endif 10 | @else 11 | 12 | @endif 13 | 14 | -------------------------------------------------------------------------------- /resources/views/livewire/auth/forgot-password.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 5 |
    6 | 7 |
    8 | 9 | {{ __('Can\'t sign in? Enter your email and we\'ll send you a link to reset your password.') }} 10 | 11 |
    12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
    20 | 21 | 22 | {{ __('Back to Sign in') }} 23 | 24 | 25 |
    26 |
    27 |
    28 | -------------------------------------------------------------------------------- /resources/views/livewire/auth/register.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 |
    17 | 18 | {{ __('Already registered?') }} 19 | 20 | 21 | {{ __('Sign in') }} 22 | 23 | 24 |
    25 |
    26 |
    27 | -------------------------------------------------------------------------------- /resources/views/livewire/auth/reset-password.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 |
    15 | 16 | 17 | {{ __('Back to Sign in') }} 18 | 19 | 20 |
    21 |
    22 |
    23 | -------------------------------------------------------------------------------- /resources/views/livewire/layout/notificationMenu.blade.php: -------------------------------------------------------------------------------- 1 |
    9 | 10 | 11 | 12 | 13 | @if (count($notifications)) 14 | 15 | @endif 16 | 17 | 18 | 19 | 20 | @forelse ($notifications as $notification) 21 |
    22 | 23 |
    24 | @empty 25 |
    26 | {{ __('No notifications') }} 27 |
    28 | @endforelse 29 |
    30 |
    31 |
    32 |
    33 | -------------------------------------------------------------------------------- /resources/views/livewire/modals/addNode.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
    7 | 8 |
    9 |
    10 |
    11 |
    12 | -------------------------------------------------------------------------------- /resources/views/livewire/modals/collaborationInvite.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

    4 | {{ __(sprintf('%s has invited you to join the vault', $username)) }} 5 | {{ $name }}. 6 |

    7 |
    8 | 14 | 21 |
    22 |
    23 |
    24 | -------------------------------------------------------------------------------- /resources/views/livewire/modals/editNode.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
    7 | 8 |
    9 |
    10 |
    11 |
    12 | -------------------------------------------------------------------------------- /resources/views/livewire/modals/importFile.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    13 | 30 | 31 | 32 |
    33 |
    34 |
    35 |
    36 | -------------------------------------------------------------------------------- /resources/views/livewire/modals/importVault.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    13 | 29 | 30 | 31 |
    32 |
    33 |
    34 |
    35 | -------------------------------------------------------------------------------- /resources/views/livewire/modals/markdownEditorTemplate.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | @if ($templates && count($templates)) 4 |
      5 | @foreach ($templates as $template) 6 |
    • 7 | 19 |
    • 20 | @endforeach 21 |
    22 | @else 23 |

    {{ __('No templates found') }}

    24 | @endif 25 |
    26 |
    27 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | (int) $user->id === $userId); 11 | 12 | Broadcast::channel('Vault.{vaultId}', function (User $user, int $vaultId): bool { 13 | $vault = Vault::find($vaultId); 14 | 15 | if ($vault === null) { 16 | return false; 17 | } 18 | 19 | return $user->can('update', $vault); 20 | }); 21 | 22 | Broadcast::channel('VaultNode.{nodeId}', function (User $user, int $nodeId): ?array { 23 | $node = VaultNode::find($nodeId); 24 | 25 | if ($node instanceof VaultNode && $user->can('update', $node->vault)) { 26 | return [ 27 | 'id' => $user->id, 28 | 'name' => $user->name, 29 | ]; 30 | } 31 | 32 | return null; 33 | }); 34 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 10 | })->purpose('Display an inspiring quote')->hourly(); 11 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | group(function (): void { 19 | Route::get('/', DashboardIndex::class)->name('dashboard.index'); 20 | 21 | Route::prefix('vaults')->group(function (): void { 22 | Route::get('/', VaultIndex::class)->name('vaults.index'); 23 | Route::get('/{vaultId}', VaultShow::class)->name('vaults.show'); 24 | }); 25 | 26 | Route::get('files/{vault}', [FileController::class, 'show'])->name('files.show'); 27 | }); 28 | 29 | Route::middleware(['guest', 'throttle'])->group(function (): void { 30 | Route::get('login', Login::class)->name('login'); 31 | 32 | if (config('settings.registration.enabled')) { 33 | Route::get('register', Register::class)->name('register'); 34 | } 35 | 36 | if (config('mail.default') !== 'log') { 37 | Route::get('forgot-password', ForgotPassword::class)->name('forgot.password'); 38 | Route::get('reset-password/{token}', ResetPassword::class)->name('password.reset'); 39 | } 40 | 41 | Route::prefix('oauth')->group(function (): void { 42 | $providers = implode('|', array_map( 43 | fn ($provider) => $provider->value, 44 | new GetAvailableOAuthProviders()->handle(), 45 | )); 46 | 47 | if ($providers !== '') { 48 | Route::get('/{provider}', OAuthLogin::class)->where('provider', $providers); 49 | Route::get('/{provider}/callback', OAuthLoginCallback::class)->where('provider', $providers); 50 | } 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !private/ 3 | !public/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Feature/Auth/DashboardTest.php: -------------------------------------------------------------------------------- 1 | get('/') 11 | ->assertRedirect(route('login')); 12 | }); 13 | 14 | it('redirects users to vaults page', function (): void { 15 | $user = User::factory()->hasVaults(1)->create(); 16 | 17 | Livewire::actingAs($user) 18 | ->test(Index::class) 19 | ->assertRedirect(route('vaults.index')); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/Feature/Auth/ForgotPasswordTest.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 12 | }); 13 | 14 | it('sends a password reset link', function (): void { 15 | $user = User::factory()->create(); 16 | 17 | Livewire::test(ForgotPassword::class) 18 | ->set('form.email', $user->email) 19 | ->call('send') 20 | ->assertSee('We have emailed your password reset link'); 21 | }); 22 | 23 | it('fails sending a password reset link', function (): void { 24 | Livewire::test(ForgotPassword::class) 25 | ->set('form.email', 'invalid@email.com') 26 | ->call('send') 27 | ->assertSee('We can\'t find a user with that email address'); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/Feature/Auth/LoginTest.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 12 | }); 13 | 14 | it('successfully authenticates user', function (): void { 15 | $user = User::factory()->create(); 16 | 17 | Livewire::test(Login::class) 18 | ->set('form.email', $user->email) 19 | ->set('form.password', 'password') 20 | ->call('send') 21 | ->assertRedirect(route('vaults.index')); 22 | }); 23 | 24 | it('gets rate limited', function (): void { 25 | $email = fake()->email(); 26 | 27 | for ($i = 0; $i < 5; $i++) { 28 | Livewire::test(Login::class) 29 | ->set('form.email', $email) 30 | ->set('form.password', 'password') 31 | ->call('send'); 32 | } 33 | 34 | Livewire::test(Login::class) 35 | ->set('form.email', $email) 36 | ->set('form.password', 'password') 37 | ->call('send') 38 | ->assertSee('Too many login attempts. Please try again in 60 seconds.'); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/Feature/Auth/OAuthLoginTest.php: -------------------------------------------------------------------------------- 1 | redirect->getTargetUrl')->andReturn($targetUrl); 14 | $availableProviders = Mockery::mock(new GetAvailableOAuthProviders()); 15 | $availableProviders->shouldReceive('handle')->andReturn([OAuthProviders::GitHub]); 16 | 17 | Livewire::test(OAuthLogin::class, ['provider' => 'github']) 18 | ->assertRedirect($targetUrl); 19 | }); 20 | 21 | it('fails redirecting to the provider url', function (): void { 22 | Socialite::shouldReceive('driver->redirect->getTargetUrl')->andThrowExceptions([new Exception()]); 23 | $availableProviders = Mockery::mock(new GetAvailableOAuthProviders()); 24 | $availableProviders->shouldReceive('handle')->andReturn([OAuthProviders::GitHub]); 25 | 26 | Livewire::test(OAuthLogin::class, ['provider' => 'github']) 27 | ->assertStatus(404); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/Feature/Auth/RegisterTest.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 11 | }); 12 | 13 | it('successfully registers an user', function (): void { 14 | $password = 'new-password'; 15 | 16 | Livewire::test(Register::class) 17 | ->set('form.name', fake()->name()) 18 | ->set('form.email', fake()->email()) 19 | ->set('form.password', $password) 20 | ->set('form.password_confirmation', $password) 21 | ->call('send') 22 | ->assertRedirect(route('vaults.index')); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/Feature/Auth/ResetPasswordTest.php: -------------------------------------------------------------------------------- 1 | create(); 12 | $token = Password::getRepository()->create($user); 13 | 14 | Livewire::test(ResetPassword::class, ['email' => $user->email, 'token' => $token]) 15 | ->assertStatus(200); 16 | }); 17 | 18 | it('resets the password', function (): void { 19 | $user = User::factory()->create(); 20 | $token = Password::getRepository()->create($user); 21 | $newPassword = 'new-password'; 22 | 23 | Livewire::test(ResetPassword::class, ['email' => $user->email, 'token' => $token]) 24 | ->set('form.email', $user->email) 25 | ->set('form.password', $newPassword) 26 | ->set('form.password_confirmation', $newPassword) 27 | ->call('send') 28 | ->assertRedirect(route('login')); 29 | }); 30 | 31 | it('fails resetting the password', function (): void { 32 | $user = User::factory()->create(); 33 | Password::getRepository()->create($user); 34 | $newPassword = 'new-password'; 35 | 36 | Livewire::test(ResetPassword::class, ['email' => $user->email, 'token' => 'invalid']) 37 | ->set('form.email', $user->email) 38 | ->set('form.password', $newPassword) 39 | ->set('form.password_confirmation', $newPassword) 40 | ->call('send') 41 | ->assertSee('This password reset token is invalid'); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/Feature/Modals/AddNodeTest.php: -------------------------------------------------------------------------------- 1 | create()->first(); 13 | $vault = new CreateVault()->handle($user, [ 14 | 'name' => fake()->words(3, true), 15 | ]); 16 | 17 | Livewire::actingAs($user) 18 | ->test(AddNode::class, ['vault' => $vault]) 19 | ->assertSet('show', false) 20 | ->call('open') 21 | ->assertSet('show', true); 22 | }); 23 | 24 | it('opens the modal providing a parent node', function (): void { 25 | $user = User::factory()->create()->first(); 26 | $vault = new CreateVault()->handle($user, [ 27 | 'name' => fake()->words(3, true), 28 | ]); 29 | $node = new CreateVaultNode()->handle($vault, [ 30 | 'is_file' => false, 31 | 'name' => fake()->words(3, true), 32 | ]); 33 | 34 | Livewire::actingAs($user) 35 | ->test(AddNode::class, ['vault' => $vault]) 36 | ->assertSet('show', false) 37 | ->call('open', $node) 38 | ->assertSet('show', true); 39 | }); 40 | 41 | it('adds a node', function (): void { 42 | $user = User::factory()->create()->first(); 43 | $vault = new CreateVault()->handle($user, [ 44 | 'name' => fake()->words(3, true), 45 | ]); 46 | expect($vault->nodes()->count())->toBe(0); 47 | 48 | Livewire::actingAs($user) 49 | ->test(AddNode::class, ['vault' => $vault]) 50 | ->call('open') 51 | ->set('form.name', fake()->words(3, true)) 52 | ->call('add') 53 | ->assertSet('show', false); 54 | 55 | expect($vault->nodes()->count())->toBe(1); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/Feature/Modals/EditNodeTest.php: -------------------------------------------------------------------------------- 1 | create()->first(); 12 | $vault = new CreateVault()->handle($user, [ 13 | 'name' => fake()->words(3, true), 14 | ]); 15 | $node = new CreateVaultNode()->handle($vault, [ 16 | 'is_file' => false, 17 | 'name' => fake()->words(3, true), 18 | ]); 19 | 20 | Livewire::actingAs($user) 21 | ->test(EditNode::class, ['vault' => $vault]) 22 | ->assertSet('show', false) 23 | ->call('open', $node) 24 | ->assertSet('show', true); 25 | }); 26 | 27 | it('updates a node', function (): void { 28 | $user = User::factory()->create()->first(); 29 | $vault = new CreateVault()->handle($user, [ 30 | 'name' => fake()->words(3, true), 31 | ]); 32 | $node = new CreateVaultNode()->handle($vault, [ 33 | 'is_file' => false, 34 | 'name' => fake()->words(3, true), 35 | ]); 36 | $newName = fake()->words(4, true); 37 | 38 | Livewire::actingAs($user) 39 | ->test(EditNode::class, ['vault' => $vault]) 40 | ->call('open', $node) 41 | ->set('form.name', $newName) 42 | ->call('edit') 43 | ->assertSet('show', false); 44 | 45 | expect($vault->nodes()->first()->name)->toBe($newName); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/Feature/Modals/MarkdownEditorSearchTest.php: -------------------------------------------------------------------------------- 1 | create()->first(); 13 | $vault = new CreateVault()->handle($user, [ 14 | 'name' => fake()->words(3, true), 15 | ]); 16 | 17 | Livewire::actingAs($user) 18 | ->test(MarkdownEditorSearch::class, ['vault' => $vault]) 19 | ->assertSet('show', false) 20 | ->call('open') 21 | ->assertSet('show', true); 22 | }); 23 | 24 | it('searches for an image file', function (): void { 25 | $user = User::factory()->create()->first(); 26 | $vault = new CreateVault()->handle($user, [ 27 | 'name' => fake()->words(3, true), 28 | ]); 29 | new CreateVaultNode()->handle($vault, [ 30 | 'is_file' => true, 31 | 'name' => 'First image', 32 | 'extension' => 'jpg', 33 | ]); 34 | new CreateVaultNode()->handle($vault, [ 35 | 'is_file' => true, 36 | 'name' => 'Second image', 37 | 'extension' => 'jpg', 38 | ]); 39 | new CreateVaultNode()->handle($vault, [ 40 | 'is_file' => true, 41 | 'name' => 'First note', 42 | 'extension' => 'md', 43 | ]); 44 | 45 | Livewire::actingAs($user) 46 | ->test(MarkdownEditorSearch::class, ['vault' => $vault]) 47 | ->call('open', 'image') 48 | ->set('search', 'first') 49 | ->assertCount('files', 1); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/Feature/Modals/SearchNodeTest.php: -------------------------------------------------------------------------------- 1 | create()->first(); 14 | $vault = new CreateVault()->handle($user, [ 15 | 'name' => fake()->words(3, true), 16 | ]); 17 | 18 | Livewire::actingAs($user) 19 | ->test(SearchNode::class, ['vault' => $vault]) 20 | ->assertSet('show', false) 21 | ->call('open') 22 | ->assertSet('show', true); 23 | }); 24 | 25 | it('searches for a node by tag', function (): void { 26 | $user = User::factory()->create()->first(); 27 | $vault = new CreateVault()->handle($user, [ 28 | 'name' => fake()->words(3, true), 29 | ]); 30 | new CreateVaultNode()->handle($vault, [ 31 | 'is_file' => true, 32 | 'name' => 'First note', 33 | 'extension' => 'md', 34 | 'content' => fake()->paragraph(), 35 | ]); 36 | $secondNode = new CreateVaultNode()->handle($vault, [ 37 | 'is_file' => true, 38 | 'name' => 'Second note', 39 | 'extension' => 'md', 40 | 'content' => fake()->paragraph() . ' #test', 41 | ]); 42 | new ProcessVaultNodeTags()->handle($secondNode); 43 | 44 | Livewire::actingAs($user) 45 | ->test(SearchNode::class, ['vault' => $vault]) 46 | ->call('open') 47 | ->set('search', 'tag:test') 48 | ->assertCount('nodes', 1); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/Feature/Vault/RowTest.php: -------------------------------------------------------------------------------- 1 | create()->first(); 14 | $vault = new CreateVault()->handle($user, [ 15 | 'name' => fake()->words(3, true), 16 | ]); 17 | $newName = fake()->words(3, true); 18 | 19 | Livewire::actingAs($user) 20 | ->test(Row::class, ['vaultId' => $vault->id]) 21 | ->set('form.name', $newName) 22 | ->call('update'); 23 | expect($user->vaults()->first()->name)->toBe($newName); 24 | 25 | $path = new GetPathFromUser()->handle($user) . $newName; 26 | expect(Storage::disk('local')->path($path))->toBeDirectory(); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/Feature/Vault/TreeViewTest.php: -------------------------------------------------------------------------------- 1 | hasVaults(1)->create()->first(); 14 | $vault = $user->vaults()->first(); 15 | 16 | Livewire::actingAs($user) 17 | ->test(TreeView::class, ['vault' => $vault]) 18 | ->assertSee('Your vault is empty.'); 19 | }); 20 | 21 | it('updates the vault', function (): void { 22 | $user = User::factory()->create()->first(); 23 | $vault = new CreateVault()->handle($user, [ 24 | 'name' => fake()->words(3, true), 25 | ]); 26 | $newName = fake()->words(3, true); 27 | 28 | Livewire::actingAs($user) 29 | ->test(TreeView::class, ['vault' => $vault]) 30 | ->set('vaultForm.name', $newName) 31 | ->call('editVault'); 32 | expect($user->vaults()->first()->name)->toBe($newName); 33 | 34 | $path = new GetPathFromUser()->handle($user) . $newName; 35 | expect(Storage::disk('local')->path($path))->toBeDirectory(); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | extend(TestCase::class) 21 | ->use(RefreshDatabase::class) 22 | ->beforeEach(function (): void { 23 | Storage::fake('local'); 24 | $this->freezeTime(); 25 | }) 26 | ->in('Feature', 'Unit'); 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Expectations 31 | |-------------------------------------------------------------------------- 32 | | 33 | | When you're writing tests, you often need to check that values meet certain conditions. The 34 | | "expect()" function gives you access to a set of "expectations" methods that you can use 35 | | to assert different things. Of course, you may extend the Expectation API at any time. 36 | | 37 | */ 38 | 39 | expect()->extend('toBeOne', fn () => $this->toBe(1)); 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Functions 44 | |-------------------------------------------------------------------------- 45 | | 46 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 47 | | project that you don't want to repeat in every file. Here you can also expose helpers as 48 | | global functions to help you to reduce the number of lines of code in your test files. 49 | | 50 | */ 51 | 52 | function something(): void 53 | { 54 | // .. 55 | } 56 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | hasVaults(1)->create(); 10 | $vault = $user->vaults()->first(); 11 | $node = $vault->nodes()->create([ 12 | 'is_file' => false, 13 | 'name' => fake()->words(3, true), 14 | ]); 15 | 16 | expect($vault->nodes()->count())->toBe(1); 17 | 18 | new DeleteVaultNode()->handle($node); 19 | 20 | expect($vault->nodes()->count())->toBe(0); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/Unit/Actions/DeleteVaultTest.php: -------------------------------------------------------------------------------- 1 | hasVaults(1)->create(); 10 | $vault = $user->vaults()->first(); 11 | 12 | expect($user->vaults()->count())->toBe(1); 13 | 14 | new DeleteVault()->handle($vault); 15 | 16 | expect($user->vaults()->count())->toBe(0); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/Unit/Actions/ExportVaultTest.php: -------------------------------------------------------------------------------- 1 | hasVaults(1)->create(); 10 | $vault = $user->vaults()->first(); 11 | $node = $vault->nodes()->create([ 12 | 'is_file' => false, 13 | 'name' => fake()->words(3, true), 14 | ]); 15 | 16 | expect($vault->nodes()->count())->toBe(1); 17 | 18 | new DeleteVaultNode()->handle($node); 19 | 20 | expect($vault->nodes()->count())->toBe(0); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/Unit/Actions/ResolveTwoPathsTest.php: -------------------------------------------------------------------------------- 1 | handle($currentPath, $path)) 12 | ->toBe('/Blog/Articles/Article.md'); 13 | }); 14 | 15 | it('resolves one absolute path and one relative path going through the root', function (): void { 16 | $currentPath = '/Personal/Inbox/Note.md'; 17 | $path = '../../Blog/Articles/Article.md'; 18 | 19 | expect(new ResolveTwoPaths()->handle($currentPath, $path)) 20 | ->toBe('/Blog/Articles/Article.md'); 21 | }); 22 | 23 | it('resolves one absolute path and one relative path not going throught the root', function (): void { 24 | $currentPath = '/Personal/Inbox/Note.md'; 25 | $path = '../Letters/Letter.md'; 26 | 27 | expect(new ResolveTwoPaths()->handle($currentPath, $path)) 28 | ->toBe('/Personal/Letters/Letter.md'); 29 | }); 30 | 31 | it('resolves paths with utf-8 characters', function (): void { 32 | $currentPath = '/'; 33 | $path = '/β.md'; 34 | 35 | expect(new ResolveTwoPaths()->handle($currentPath, $path)) 36 | ->toBe('/β.md'); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/Unit/Console/UpgradeTest.php: -------------------------------------------------------------------------------- 1 | artisan('upgrade:run')->assertExitCode(0); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/Unit/Models/TagTest.php: -------------------------------------------------------------------------------- 1 | create()->refresh(); 9 | 10 | expect(array_keys($tag->toArray())) 11 | ->toBe([ 12 | 'id', 13 | 'name', 14 | 'created_at', 15 | 'updated_at', 16 | ]); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/Unit/Models/UserTest.php: -------------------------------------------------------------------------------- 1 | create()->refresh(); 10 | 11 | expect(array_keys($user->toArray())) 12 | ->toBe([ 13 | 'id', 14 | 'name', 15 | 'email', 16 | 'email_verified_at', 17 | 'created_at', 18 | 'updated_at', 19 | 'last_visited_url', 20 | ]); 21 | }); 22 | 23 | it('may have vaults', function (): void { 24 | $user = User::factory()->hasVaults(3)->create(); 25 | 26 | expect($user->vaults)->toHaveCount(3) 27 | ->each->toBeInstanceOf(Vault::class); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/Unit/Models/VaultTest.php: -------------------------------------------------------------------------------- 1 | create()->refresh(); 11 | 12 | expect(array_keys($vault->toArray())) 13 | ->toBe([ 14 | 'id', 15 | 'name', 16 | 'created_by', 17 | 'created_at', 18 | 'updated_at', 19 | 'templates_node_id', 20 | ]); 21 | }); 22 | 23 | it('belongs to a user', function (): void { 24 | $vault = Vault::factory()->create(); 25 | 26 | expect($vault->user)->toBeInstanceOf(User::class); 27 | }); 28 | 29 | it('may have nodes', function (): void { 30 | $vault = Vault::factory()->hasNodes(3)->create(); 31 | 32 | expect($vault->nodes)->toHaveCount(3) 33 | ->each->toBeInstanceOf(VaultNode::class); 34 | }); 35 | 36 | it('may have a templates node', function (): void { 37 | $vault = Vault::factory()->hasNodes(3)->create(); 38 | 39 | $vault->update(['templates_node_id' => $vault->nodes->get(1)->id]); 40 | 41 | expect($vault->templatesNode)->toBeInstanceOf(VaultNode::class); 42 | }); 43 | -------------------------------------------------------------------------------- /typesense/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | laravel({ 8 | input: ['resources/css/app.css', 'resources/js/app.js'], 9 | refresh: true, 10 | }), 11 | tailwindcss(), 12 | ], 13 | }); 14 | --------------------------------------------------------------------------------