├── .dockerignore ├── .editorconfig ├── .env.docker.example ├── .env.example ├── .env.sqlite.example ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .styleci.yml ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── api.md ├── app.json ├── app ├── Console │ ├── Commands │ │ ├── .gitkeep │ │ ├── ExportCommand.php │ │ ├── FixPositionCommand.php │ │ ├── InstallCommand.php │ │ ├── RetryMigrationCommand.php │ │ ├── RunBackupCommand.php │ │ └── ThumbnailCommand.php │ └── Kernel.php ├── Exceptions │ └── Handler.php ├── Helpers │ ├── NetscapeBookmarkDecoder.php │ └── NetscapeBookmarkEncoder.php ├── Http │ ├── Controllers │ │ ├── AuthController.php │ │ ├── CollectionController.php │ │ ├── Controller.php │ │ ├── ExportController.php │ │ ├── FileController.php │ │ ├── ImportController.php │ │ ├── PostController.php │ │ ├── PublicShareController.php │ │ ├── TagController.php │ │ ├── TestingController.php │ │ └── UserController.php │ ├── Kernel.php │ └── Middleware │ │ ├── Authenticate.php │ │ ├── E2ETesting.php │ │ ├── EncryptCookies.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── TrimStrings.php │ │ ├── TrustHosts.php │ │ ├── TrustProxies.php │ │ └── VerifyCsrfToken.php ├── Jobs │ └── ProcessMissingThumbnail.php ├── Models │ ├── Collection.php │ ├── Post.php │ ├── PostTag.php │ ├── PublicShare.php │ ├── Tag.php │ └── User.php ├── Notifications │ └── ResetPassword.php ├── Policies │ ├── CollectionPolicy.php │ ├── PostPolicy.php │ ├── PublicSharePolicy.php │ ├── TagPolicy.php │ └── UserPolicy.php ├── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── BroadcastServiceProvider.php │ ├── EventServiceProvider.php │ └── RouteServiceProvider.php └── Services │ ├── CollectionService.php │ ├── PostService.php │ └── TagService.php ├── artisan ├── bootstrap ├── app.php └── cache │ └── .gitignore ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── benotes.php ├── broadcasting.php ├── cache.php ├── cors.php ├── database.php ├── filesystems.php ├── hashing.php ├── image.php ├── jwt.php ├── logging.php ├── mail.php ├── queue.php ├── sanctum.php ├── services.php ├── session.php └── view.php ├── database ├── .gitignore ├── factories │ ├── CollectionFactory.php │ ├── PostFactory.php │ ├── TagFactory.php │ └── UserFactory.php ├── migrations │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2019_08_28_093831_create_collections_table.php │ ├── 2020_02_29_231807_create_posts_table.php │ ├── 2020_03_04_132210_remove_unused_columns_and_add_permissions_to_users.php │ ├── 2020_03_23_150126_create_shares_table.php │ ├── 2020_12_20_133609_change_column_type_from_posts.php │ ├── 2021_07_24_224526_change_color_from_posts.php │ ├── 2022_03_04_162953_add_icon_to_collections_table.php │ ├── 2022_07_18_233713_create_tags_table.php │ ├── 2022_07_18_234358_create_post_tag_table.php │ ├── 2022_12_12_000000_create_failed_jobs_table.php │ ├── 2022_12_14_000001_create_personal_access_tokens_table.php │ ├── 2023_01_28_103727_add_parent_id_to_collections_table.php │ └── 2023_02_26_103159_create_jobs_table.php └── seeders │ └── DatabaseSeeder.php ├── docker-compose.yml ├── docker ├── crontab ├── entrypoint.sh ├── install.sh ├── nginx-laravel.conf ├── nginx.conf ├── php-fpm.conf ├── php.ini ├── supervisord.ini └── update.sh ├── nginx.conf ├── package-lock.json ├── package.json ├── phpunit.xml ├── playwright.config.js ├── playwright ├── beforeEach.js ├── global-setup.js ├── global-teardown.js └── tests │ └── posts.spec.js ├── public ├── .htaccess ├── css │ ├── app.css │ └── inter.css ├── fonts │ ├── Inter-Black.woff │ ├── Inter-Black.woff2 │ ├── Inter-BlackItalic.woff │ ├── Inter-BlackItalic.woff2 │ ├── Inter-Bold.woff │ ├── Inter-Bold.woff2 │ ├── Inter-BoldItalic.woff │ ├── Inter-BoldItalic.woff2 │ ├── Inter-ExtraBold.woff │ ├── Inter-ExtraBold.woff2 │ ├── Inter-ExtraBoldItalic.woff │ ├── Inter-ExtraBoldItalic.woff2 │ ├── Inter-ExtraLight.woff │ ├── Inter-ExtraLight.woff2 │ ├── Inter-ExtraLightItalic.woff │ ├── Inter-ExtraLightItalic.woff2 │ ├── Inter-Italic.woff │ ├── Inter-Italic.woff2 │ ├── Inter-Light.woff │ ├── Inter-Light.woff2 │ ├── Inter-LightItalic.woff │ ├── Inter-LightItalic.woff2 │ ├── Inter-Medium.woff │ ├── Inter-Medium.woff2 │ ├── Inter-MediumItalic.woff │ ├── Inter-MediumItalic.woff2 │ ├── Inter-Regular.woff │ ├── Inter-Regular.woff2 │ ├── Inter-SemiBold.woff │ ├── Inter-SemiBold.woff2 │ ├── Inter-SemiBoldItalic.woff │ ├── Inter-SemiBoldItalic.woff2 │ ├── Inter-Thin.woff │ ├── Inter-Thin.woff2 │ ├── Inter-ThinItalic.woff │ ├── Inter-ThinItalic.woff2 │ ├── Inter-italic.var.woff2 │ ├── Inter-roman.var.woff2 │ └── Inter.var.woff2 ├── glyphs.svg ├── index.php ├── js │ ├── app.js │ └── app.js.LICENSE.txt ├── logo_144x144.png ├── manifest.json ├── mix-manifest.json ├── robots.txt └── service-worker.js ├── resources ├── js │ ├── UnfurlingLink.js │ ├── api │ │ ├── auth.js │ │ ├── collection.js │ │ └── post.js │ ├── app.js │ ├── components │ │ ├── App.vue │ │ ├── Appbar.vue │ │ ├── BottomSheet.vue │ │ ├── CollectionMenu.vue │ │ ├── CollectionSidebar.vue │ │ ├── Deselect.vue │ │ ├── EditorMenuBar.vue │ │ ├── IconPicker.vue │ │ ├── Notification.vue │ │ ├── OpenIndicator.vue │ │ ├── PostContextMenu.vue │ │ ├── PostItem.vue │ │ ├── PostItemLink.vue │ │ ├── PostItemPlaceholder.vue │ │ ├── PostItemTags.vue │ │ ├── PostItemText.vue │ │ ├── PostLoader.vue │ │ ├── Searchbar.vue │ │ ├── Sidebar.vue │ │ ├── UnfurlingLink.vue │ │ └── pages │ │ │ ├── Collection.vue │ │ │ ├── EditCollection.vue │ │ │ ├── EditTag.vue │ │ │ ├── ExportBookmarks.vue │ │ │ ├── Forgot.vue │ │ │ ├── ImExport.vue │ │ │ ├── ImportBookmarks.vue │ │ │ ├── Login.vue │ │ │ ├── Post.vue │ │ │ ├── Reset.vue │ │ │ ├── Restore.vue │ │ │ ├── Search.vue │ │ │ ├── Tag.vue │ │ │ ├── Tags.vue │ │ │ ├── User.vue │ │ │ └── Users.vue │ ├── routes.js │ ├── service-worker.js │ └── store │ │ ├── index.js │ │ └── modules │ │ ├── appbar.js │ │ ├── auth.js │ │ ├── collection.js │ │ ├── notification.js │ │ └── post.js ├── json │ └── glyphs.json ├── lang │ └── en │ │ ├── auth.php │ │ ├── pagination.php │ │ ├── passwords.php │ │ └── validation.php ├── sass │ ├── app.scss │ ├── theme.scss │ └── themes │ │ └── dark.scss ├── svg │ ├── arrow_down.svg │ ├── bookmark_export.svg │ ├── glyphs │ │ ├── 4003.svg │ │ ├── 4008.svg │ │ ├── 4010.svg │ │ ├── 4017.svg │ │ └── 4103.svg │ ├── logo_64x64.svg │ ├── material │ │ ├── arrow_drop_down.svg │ │ ├── art_track.svg │ │ ├── autorenew.svg │ │ ├── check_box.svg │ │ ├── clear.svg │ │ ├── code.svg │ │ ├── format_bold.svg │ │ ├── format_italic.svg │ │ ├── format_quote.svg │ │ ├── format_underlined.svg │ │ ├── horizontal_rule.svg │ │ ├── image.svg │ │ ├── label.svg │ │ ├── link.svg │ │ ├── list_bulleted.svg │ │ ├── list_numbered.svg │ │ ├── redo.svg │ │ └── undo.svg │ ├── remix │ │ ├── arrow-down-s-line.svg │ │ ├── folder-3-line.svg │ │ ├── folder-add-fill.svg │ │ ├── folder-fill.svg │ │ ├── folder-unknow-fill.svg │ │ ├── git-repository-commits-line.svg │ │ ├── group-fill.svg │ │ ├── inbox-unarchive-line.svg │ │ ├── logout-circle-line.svg │ │ ├── menu-line.svg │ │ ├── more-2-fill.svg │ │ ├── refresh-line.svg │ │ ├── search-line.svg │ │ ├── settings-3-line.svg │ │ ├── upload-2-fill.svg │ │ └── user-settings-fill.svg │ └── zondicons │ │ ├── add-outline.svg │ │ ├── add-solid.svg │ │ ├── checkmark-outline.svg │ │ ├── paste.svg │ │ ├── stand-by.svg │ │ ├── text-decoration.svg │ │ └── trash.svg └── views │ ├── app.blade.php │ └── stubs │ └── env.blade.php ├── routes ├── api.php ├── channels.php ├── console.php └── web.php ├── server.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ ├── .gitignore │ │ └── thumbnails │ │ └── .gitignore ├── backup │ └── .gitignore ├── database.sqlite ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore ├── logs │ └── .gitignore └── tmp │ └── .gitignore ├── tailwind.config.js ├── tests ├── CreatesApplication.php ├── Feature │ ├── CollectionTest.php │ ├── ImAndExportTest.php │ ├── PostTest.php │ ├── ShareTest.php │ ├── TagTest.php │ └── UserTest.php ├── TestCase.php ├── Unit │ └── ExampleTest.php └── bookmarks.html └── webpack.mix.js /.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /notes 3 | /playwright 4 | /.vscode 5 | npm-debug.log 6 | .phpunit.result.cache 7 | Dockerfile* 8 | docker-compose* 9 | .dockerignore 10 | .git 11 | .gitignore 12 | README.md 13 | /storage/app/public/thumbnails 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 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.docker.example: -------------------------------------------------------------------------------- 1 | APP_PORT=8000 2 | 3 | APP_NAME=Benotes 4 | APP_ENV=local 5 | APP_DEBUG=false 6 | APP_URL=http://localhost:${APP_PORT} 7 | APP_TIMEZONE=UTC 8 | 9 | APP_KEY= 10 | JWT_SECRET= 11 | 12 | GENERATE_MISSING_THUMBNAILS=true 13 | USE_FILESYSTEM=true 14 | RUN_BACKUP=false 15 | 16 | DB_CONNECTION=mysql 17 | DB_HOST=db 18 | DB_PORT=3306 19 | DB_DATABASE=benotes 20 | DB_USERNAME=benotes 21 | DB_PASSWORD=benotes 22 | 23 | CACHE_DRIVER=file 24 | 25 | MAIL_DRIVER=smtp 26 | MAIL_HOST= 27 | MAIL_PORT=587 28 | MAIL_USERNAME= 29 | MAIL_PASSWORD= 30 | MAIL_ENCRYPTION=tls 31 | MAIL_FROM_ADDRESS= 32 | MAIL_FROM_NAME="Benotes" -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Benotes 2 | APP_ENV=local 3 | APP_DEBUG=false 4 | APP_URL=http://localhost 5 | 6 | APP_KEY= 7 | JWT_SECRET= 8 | GENERATE_MISSING_THUMBNAILS=false 9 | USE_FILESYSTEM=true 10 | RUN_BACKUP=false 11 | 12 | DB_CONNECTION=mysql 13 | DB_HOST=127.0.0.1 14 | DB_PORT=3306 15 | DB_DATABASE= 16 | DB_USERNAME= 17 | DB_PASSWORD= 18 | 19 | MAIL_DRIVER=smtp 20 | MAIL_HOST= 21 | MAIL_PORT=587 22 | MAIL_USERNAME= 23 | MAIL_PASSWORD= 24 | MAIL_ENCRYPTION=tls 25 | MAIL_FROM_ADDRESS= -------------------------------------------------------------------------------- /.env.sqlite.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Benotes 2 | APP_ENV=production 3 | APP_DEBUG=false 4 | APP_URL=http://localhost:8000 5 | 6 | APP_KEY= 7 | JWT_SECRET= 8 | 9 | GENERATE_MISSING_THUMBNAILS=true 10 | USE_FILESYSTEM=true 11 | 12 | DB_CONNECTION=sqlite 13 | DB_HOST= 14 | DB_PORT= 15 | DB_DATABASE=/var/www/storage/database.sqlite 16 | DB_USERNAME= 17 | DB_PASSWORD= 18 | 19 | MAIL_DRIVER=smtp 20 | MAIL_HOST= 21 | MAIL_PORT=587 22 | MAIL_USERNAME= 23 | MAIL_PASSWORD= 24 | MAIL_ENCRYPTION=tls 25 | MAIL_FROM_ADDRESS= 26 | MAIL_FROM_NAME="Benotes" 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: ['eslint:recommended', 'plugin:vue/recommended', 'prettier'], 7 | plugins: ['prettier'], 8 | 'prettier/prettier': ['error', { endOfLine: 'lf' }], 9 | globals: { 10 | Atomics: 'readonly', 11 | SharedArrayBuffer: 'readonly', 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 2018, 15 | sourceType: 'module', 16 | }, 17 | rules: { 18 | indent: ['error', 4], 19 | 'vue/multi-word-component-names': 'off', 20 | 'prettier/prettier': 'warn', 21 | }, 22 | ignorePatterns: ['.eslintrc.js', 'webpack.mix.js', 'vendor/', 'public/', 'notes/'], 23 | } 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | * text eol=lf 3 | 4 | *.blade.php diff=html 5 | *.css diff=css 6 | *.html diff=html 7 | *.md diff=markdown 8 | *.php diff=php 9 | 10 | /.github export-ignore 11 | CHANGELOG.md export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /notes 2 | /node_modules 3 | /public/hot 4 | /public/storage 5 | /storage/*.key 6 | /vendor 7 | .env 8 | .env.backup 9 | .env.playwright 10 | .phpunit.result.cache 11 | docker-compose.override.yml 12 | Homestead.json 13 | Homestead.yaml 14 | npm-debug.log 15 | yarn-error.log 16 | /.idea 17 | /.vscode 18 | /test-results/ 19 | /playwright-report/ 20 | /playwright/.cache/ 21 | /test-results/ 22 | /playwright-report/ 23 | /playwright/.cache/ 24 | /playwright/state.json 25 | *.zip 26 | 27 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /public 3 | /notes 4 | *.json 5 | *.lock 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 4, 4 | semi: false, 5 | singleQuote: true, 6 | bracketSameLine: true, 7 | printWidth: 90, 8 | end_of_line: 'lf', 9 | } 10 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | php: 2 | preset: laravel 3 | version: 8 4 | disabled: 5 | - no_unused_imports 6 | finder: 7 | not-name: 8 | - index.php 9 | - server.php 10 | js: 11 | finder: 12 | not-name: 13 | - webpack.mix.js 14 | css: true 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 fr0tt 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: vendor/bin/heroku-php-nginx -C nginx.conf public/ 2 | 3 | release: php artisan migrate --force && php artisan cache:clear -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Benotes Logo 4 |

5 | 6 |

Benotes

7 | 8 | Benotes Thumbnail 10 | 11 | An open source self hosted web app for your notes and bookmarks side by side. 12 | 13 | This project is currently in **Beta**. You may encounter bugs or errors. 14 | 15 | ### Features 16 | 17 | - URLs are automatically saved with an image, title and description 18 | - supports both markdown and a rich text editor 19 | - can be installed as a PWA on your mobile devices (and desktop) 20 | - share content via this app (if installed as an PWA and supported by your browser) 21 | - collections can be shared via a public available URL 22 | - links can be instantly pasted as new posts 23 | - can be hosted almost anywhere thanks to its use of the lightweight Lumen framework and well supported PHP language 24 | - works with and without a persistent storage layer (both filesystem and S3 are supported) 25 | - can also be hosted via Docker or on Heroku 26 | - protect your data with daily backups 27 | 28 | ## Installation & Upgrade 29 | 30 | Currently their are three options for you to choose from: 31 | 32 | - [Normal classical way](https://benotes.org/docs/installation/classic) 33 | - [Docker](https://benotes.org/docs/installation/docker) 34 | - [Docker Compose](https://benotes.org/docs/installation/docker-compose) 35 | - [Heroku](https://benotes.org/docs/installation/heroku) ([not free anymore](https://blog.heroku.com/next-chapter)) 36 | 37 | ## Additional Features 38 | 39 | - [Backups](https://benotes.org/docs/extras/backup) 40 | - [Bookmarklet](https://benotes.org/docs/extras/bookmarklet) 41 | 42 | ## Issues 43 | 44 | Feel free to [contact me](https://twitter.com/_fr0tt) if you need any help or open an [issue](https://github.com/fr0tt/benotes/issues) or a [discussion](https://github.com/fr0tt/benotes/discussions) or join the [subreddit](https://reddit.com/r/benotes). 45 | 46 | Q: Having trouble with **reordering** posts ? 47 | 48 | Use this command in order to fix it. 49 | 50 | ``` 51 | php artisan fix-position 52 | ``` 53 | 54 | or if you have already installed newer php versions on your system: 55 | 56 | ``` 57 | /usr/bin/php7.4 artisan fix-position 58 | ``` 59 | 60 | ## Rest API 61 | 62 | Further information can be found here: [Rest API Documentation](api.md) 63 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Benotes", 3 | "description": "An open source web app for your notes & bookmarks.", 4 | "repository": "https://github.com/fr0tt/benotes", 5 | "logo": "https://raw.githubusercontent.com/fr0tt/benotes/master/public/logo_144x144.png", 6 | "keywords": [ 7 | "laravel", 8 | "lumen", 9 | "vue" 10 | ], 11 | "addons": [ 12 | "heroku-postgresql" 13 | ], 14 | "scripts": { 15 | "postdeploy": [] 16 | }, 17 | "env": { 18 | "APP_NAME": "Benotes", 19 | "APP_ENV": "production", 20 | "APP_DEBUG": "false", 21 | "APP_KEY": { 22 | "generator": "secret" 23 | }, 24 | "JWT_SECRET": { 25 | "generator": "secret" 26 | }, 27 | "USE_FILESYSTEM": "false", 28 | "TRUSTED_PROXIES": "*", 29 | "LOG_CHANNEL": "errorlog", 30 | "DB_CONNECTION": "pgsql" 31 | } 32 | } -------------------------------------------------------------------------------- /app/Console/Commands/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/app/Console/Commands/.gitkeep -------------------------------------------------------------------------------- /app/Console/Commands/ExportCommand.php: -------------------------------------------------------------------------------- 1 | info('Export a collection:'); 36 | $collection_id = $this->ask('Collection Id'); 37 | $email = $this->ask('Email address of its owner'); 38 | 39 | $validator = Validator::make([ 40 | 'collection_id' => $collection_id, 41 | 'email' => $email 42 | ], [ 43 | 'collection_id' => 'integer', 44 | 'email' => 'email', 45 | ]); 46 | 47 | if ($validator->fails()) { 48 | foreach ($validator->errors()->all() as $error) { 49 | $this->error($error); 50 | } 51 | return; 52 | } 53 | 54 | $user = User::where('email', $email)->firstOrFail(); 55 | 56 | $collection = Collection::where('id', $collection_id)->where('user_id', $user->id)->firstOrFail(); 57 | 58 | $posts = Post::select('title', 'content') 59 | ->where('collection_id', $collection_id) 60 | ->whereNull('deleted_at') 61 | ->orderBy('order', 'desc') 62 | ->get(); 63 | 64 | $filename = date('Y-m-d') . '_' . $collection->name . '.json'; 65 | file_put_contents('database/data/' . $filename, json_encode($posts, JSON_PRETTY_PRINT)); 66 | 67 | $this->line(PHP_EOL); 68 | $this->info('Export completed.'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/Console/Commands/FixPositionCommand.php: -------------------------------------------------------------------------------- 1 | get(); 36 | foreach ($users as $user) { 37 | $user_id = $user->id; 38 | $collections = Collection::select('id', 'name') 39 | ->where('user_id', $user_id) 40 | ->get(); 41 | $uncategorized = new stdClass; 42 | $uncategorized->id = null; 43 | $uncategorized->name = 'Uncategorized'; 44 | $collections->push($uncategorized); 45 | foreach ($collections as $collection) { 46 | echo '------- ' . $collection->name . ' by ' . $user->name . ':' . ' -------' . PHP_EOL; 47 | $collection_id = $collection->id; 48 | $posts = Post::where('user_id', $user_id) 49 | ->where('collection_id', $collection_id) 50 | ->where('deleted_at', null) 51 | ->orderBy('order') 52 | ->get(); 53 | for ($i = 0; $i < $posts->count(); $i++) { 54 | $post = $posts[$i]; 55 | $post->order = $i + 1; 56 | $post->save(); 57 | echo $post['order'] . ': ' . $post['title'] . PHP_EOL; 58 | }; 59 | } 60 | } 61 | 62 | $this->info('completed.'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Console/Commands/RetryMigrationCommand.php: -------------------------------------------------------------------------------- 1 | info('Migrate database...'); 45 | 46 | for ($i = 0; $i < 3; $i++) { 47 | 48 | try { 49 | DB::connection()->getPDO(); 50 | Artisan::call('migrate --force'); 51 | $this->info('Database migration completed.'); 52 | return 0; 53 | } catch(\Exception $e) { 54 | sleep(5 + $i); 55 | $this->info('Database migration failed. Try again...'); 56 | } 57 | } 58 | 59 | try { 60 | DB::connection()->getPDO(); 61 | Artisan::call('migrate --force'); 62 | $this->info('Database migration completed.'); 63 | return 0; 64 | } catch(\Exception $e) { 65 | $this->error('Database migration failed.'); 66 | Log::error('Database migration failed.'); 67 | } 68 | 69 | 70 | return 0; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/Console/Commands/ThumbnailCommand.php: -------------------------------------------------------------------------------- 1 | service = new PostService; 36 | } 37 | 38 | /** 39 | * Execute the console command. 40 | * 41 | * @return int 42 | */ 43 | public function handle() 44 | { 45 | 46 | $post_id = $this->argument('id'); 47 | if (empty($post_id)) { 48 | $this->info('What post would you like to "improve" ?'); 49 | $post_id = $this->ask('Please specify a post id or type all'); 50 | } 51 | 52 | if ($post_id === 'all') { 53 | $posts = Post::whereNull('deleted_at') 54 | ->where('type', Post::POST_TYPE_LINK) 55 | ->whereNull('image_path'); 56 | $this->info($posts->count() . ' potential posts found. This could take several minutes.'); 57 | foreach ($posts->get() as $post) { 58 | $this->info('Process post ' . $post->id . '...'); 59 | $this->createThumbnail($post); 60 | } 61 | return 0; 62 | } else { 63 | $post = Post::find(intval($post_id)); 64 | $this->createThumbnail($post); 65 | } 66 | 67 | return 0; 68 | } 69 | 70 | private function createThumbnail(Post $post) 71 | { 72 | if ($post->type === Post::POST_TYPE_TEXT) { 73 | $this->error('Post is not a link and therefore has no thumbnail'); 74 | return; 75 | } 76 | 77 | if (@get_headers($post->url) == false) { 78 | $this->error('Post has no existing link'); 79 | return; 80 | } 81 | 82 | $filename = $this->service->generateThumbnailFilename($post->url, $post->id); 83 | $path = $this->service->getThumbnailPath($filename); 84 | $this->service->crawlWithChrome($filename, $path, $post->url, $post->id); 85 | if (file_exists($path)) { 86 | $post->image_path = $filename; 87 | $post->save(); 88 | } else { 89 | $this->error('Thumbnail could not be created'); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('backup:run')->cron(config('benotes.backup_interval')); 23 | } 24 | if (config('benotes.generate_missing_thumbnails')) { 25 | $schedule->command('queue:work --max-jobs=10 --tries=3 --stop-when-empty') 26 | ->cron(config('benotes.thumbnail_filler_interval')); 27 | } 28 | } 29 | 30 | /** 31 | * Register the commands for the application. 32 | * 33 | * @return void 34 | */ 35 | protected function commands() 36 | { 37 | $this->load(__DIR__ . '/Commands'); 38 | 39 | require base_path('routes/console.php'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | protected $dontReport = [ 18 | AuthorizationException::class, 19 | HttpException::class, 20 | //ModelNotFoundException::class, 21 | ValidationException::class, 22 | ]; 23 | 24 | /** 25 | * A list of the inputs that are never flashed for validation exceptions. 26 | * 27 | * @var array 28 | */ 29 | protected $dontFlash = [ 30 | 'current_password', 31 | 'password', 32 | 'password_confirmation', 33 | ]; 34 | 35 | 36 | /** 37 | * Register the exception handling callbacks for the application. 38 | * 39 | * @return void 40 | */ 41 | public function register() 42 | { 43 | $this->reportable(function (Throwable $e) { 44 | // 45 | }); 46 | } 47 | 48 | /** 49 | * Render an exception into an HTTP response. 50 | * 51 | * @param \Illuminate\Http\Request $request 52 | * @param \Throwable $exception 53 | * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse 54 | * 55 | * @throws \Throwable 56 | */ 57 | public function render($request, Throwable $exception) 58 | { 59 | if ($exception instanceof \PHPOpenSourceSaver\JWTAuth\Exceptions\TokenExpiredException) { 60 | return response()->json('Token has expired', 401); 61 | } else if ( 62 | $exception instanceof \PHPOpenSourceSaver\JWTAuth\Exceptions\TokenBlacklistedException || 63 | $exception instanceof \PHPOpenSourceSaver\JWTAuth\Exceptions\TokenInvalidException 64 | ) { 65 | return response()->json('Token is invalid', 401); 66 | } else if ($exception instanceof \Intervention\Image\Exception\NotWritableException) { 67 | return response()->json('Storage path not writable.', 403); 68 | } else if ($exception instanceof AuthorizationException) { 69 | return response()->json('This action is unauthorized.', 403); 70 | } else if ($exception instanceof ModelNotFoundException) { 71 | return response()->json( 72 | str_replace('App\\', '', $exception->getModel()) . ' not found.', 73 | 404 74 | ); 75 | } 76 | 77 | return parent::render($request, $exception); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | json('Missing write permission', Response::HTTP_BAD_GATEWAY); 16 | } 17 | 18 | $tempDirectory = (new TemporaryDirectory(config('benotes.temporary_directory'))) 19 | ->name('export') 20 | ->force() 21 | ->create() 22 | ->empty(); 23 | 24 | $path = $tempDirectory->path('export.html'); 25 | $encoder = new NetscapeBookmarkEncoder(Auth()->user()->id); 26 | $encoder->encodeToFile($path); 27 | 28 | return response()->download($path)->deleteFileAfterSend(true); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/Http/Controllers/FileController.php: -------------------------------------------------------------------------------- 1 | validate($request, [ 17 | 'file' => 'image|required', 18 | ]); 19 | 20 | $path = $request->file('file')->store('attachments'); 21 | 22 | Image::make(Storage::path($path)) 23 | ->resize(1600, null, function ($constraint) { 24 | $constraint->aspectRatio(); 25 | $constraint->upsize(); 26 | }) 27 | ->interlace() 28 | ->save(); 29 | 30 | return response()->json([ 31 | 'data' => [ 32 | 'path' => Storage::url($path) 33 | ] 34 | ], Response::HTTP_CREATED); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/Http/Controllers/ImportController.php: -------------------------------------------------------------------------------- 1 | service = new PostService(); 19 | } 20 | 21 | public function store(Request $request) 22 | { 23 | 24 | $this->validate($request, [ 25 | 'file' => 'file|mimetypes:text/html|required', 26 | ]); 27 | 28 | $collection = Collection::firstOrCreate([ 29 | 'name' => Collection::IMPORTED_COLLECTION_NAME, 30 | 'user_id' => Auth()->user()->id 31 | ]); 32 | 33 | $parser = new NetscapeBookmarkDecoder(Auth()->user()->id); 34 | $parser->parseFile($request->file('file'), $collection->id); 35 | 36 | return response()->json('', Response::HTTP_CREATED); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/Http/Controllers/TestingController.php: -------------------------------------------------------------------------------- 1 | middleware('testing'); 17 | } 18 | 19 | public function user(Request $request) 20 | { 21 | $request->validate([ 22 | 'email' => 'email|required', 23 | 'password' => 'required' 24 | ]); 25 | 26 | $user = User::factory()->create([ 27 | 'email' => $request->email, 28 | 'password' => Hash::make($request->password), 29 | 'permission' => 7 30 | ]); 31 | return response()->json(['data' => $user]); 32 | } 33 | 34 | public function setup() 35 | { 36 | Artisan::call('config:clear'); 37 | Artisan::call('migrate:fresh'); 38 | return response()->json(null, Response::HTTP_OK); 39 | } 40 | 41 | public function teardown() 42 | { 43 | Artisan::call('config:clear'); 44 | return response()->json(null, Response::HTTP_OK); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 22 | return route('login'); 23 | } 24 | */ 25 | } 26 | 27 | /** 28 | * Handle an incoming request. 29 | * 30 | * @param \Illuminate\Http\Request $request 31 | * @param \Closure $next 32 | * @param string[] ...$guards 33 | * @return mixed 34 | * 35 | * @throws \Illuminate\Auth\AuthenticationException 36 | */ 37 | public function handle($request, Closure $next, ...$guards) 38 | { 39 | if (in_array(/*'auth:sanctum'*/'api', $guards) && Auth::guard('api')->check()) { 40 | return $next($request); 41 | } 42 | 43 | if (in_array('share', $guards)) { 44 | if (Auth::guard('share')->check()) { 45 | config()->set('auth.defaults.guard', 'share'); 46 | return $next($request); 47 | } 48 | } 49 | //$this->authenticate($request, $guards); 50 | return response('', Response::HTTP_UNAUTHORIZED); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Http/Middleware/E2ETesting.php: -------------------------------------------------------------------------------- 1 | hasHeader('X-Playwright')) { 22 | // abort(Response::HTTP_FORBIDDEN); 23 | // } 24 | if (app()->environment('production')) { 25 | abort(Response::HTTP_FORBIDDEN); 26 | } 27 | if (env('ALLOW_E2E_TESTING') !== true) { 28 | abort(Response::HTTP_FORBIDDEN); 29 | } 30 | 31 | return $next($request); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 26 | return redirect(RouteServiceProvider::HOME); 27 | } 28 | } 29 | 30 | return $next($request); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts() 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Jobs/ProcessMissingThumbnail.php: -------------------------------------------------------------------------------- 1 | post = $post->withoutRelations(); 36 | $this->service = new PostService(); 37 | } 38 | 39 | /** 40 | * Get the middleware the job should pass through. 41 | * 42 | * @return array 43 | */ 44 | public function middleware() 45 | { 46 | return [(new WithoutOverlapping($this->post->id))->releaseAfter(60)]; 47 | } 48 | 49 | /** 50 | * Execute the job. 51 | * 52 | * @return void 53 | */ 54 | public function handle() 55 | { 56 | if ($this->post->type === Post::POST_TYPE_TEXT) { 57 | return; 58 | } 59 | if (!empty($this->post->image_path)) { 60 | return; 61 | } 62 | if (!empty($this->post->deleted_at)) { 63 | return; 64 | } 65 | if (@get_headers($this->post->url) == false) { 66 | return; 67 | } 68 | 69 | $filename = $this->service->generateThumbnailFilename($this->post->url, $this->post->id); 70 | $path = $this->service->getThumbnailPath($filename); 71 | $this->service->crawlWithChrome($filename, $path, $this->post->url, $this->post->id); 72 | 73 | if (file_exists($path)) { 74 | $this->post->image_path = $filename; 75 | $this->post->save(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/Models/Collection.php: -------------------------------------------------------------------------------- 1 | 'integer', 23 | // because of SQLite 24 | 'icon_id' => 'integer', 25 | 'parent_id' => 'integer', 26 | ]; 27 | 28 | /** 29 | * The attributes that are mass assignable. 30 | * 31 | * @var array 32 | */ 33 | protected $fillable = [ 34 | 'name', 35 | 'user_id', 36 | 'icon_id', 37 | 'parent_id', 38 | 'root_id' 39 | ]; 40 | 41 | /** 42 | * The attributes excluded from the model's JSON form. 43 | * 44 | * @var array 45 | */ 46 | protected $hidden = [ 47 | 'user_id', 48 | 'deleted_at' 49 | ]; 50 | 51 | public static function getCollectionId($id, $is_uncategorized = false) 52 | { 53 | return $is_uncategorized || $id === null ? null : intval($id); 54 | } 55 | 56 | public function parent(): Relations\BelongsTo 57 | { 58 | return $this->belongsTo(self::class, 'parent_id'); 59 | } 60 | 61 | public function children(): Relations\HasMany 62 | { 63 | return $this->hasMany(self::class, 'parent_id'); 64 | } 65 | 66 | public function nested(): Relations\HasMany 67 | { 68 | return $this->children()->with('nested')->orderBy('name'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/Models/PostTag.php: -------------------------------------------------------------------------------- 1 | 'integer', // because of SQLite 25 | 'post_id' => 'integer', // because of SQLite 26 | ]; 27 | 28 | /** 29 | * The attributes that are mass assignable. 30 | * 31 | * @var array 32 | */ 33 | protected $fillable = [ 34 | 'post_id', 'tag_id' 35 | ]; 36 | 37 | /** 38 | * The attributes excluded from the model's JSON form. 39 | * 40 | * @var array 41 | */ 42 | protected $hidden = []; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/Models/PublicShare.php: -------------------------------------------------------------------------------- 1 | 'boolean', 23 | ]; 24 | 25 | /** 26 | * The attributes that are mass assignable. 27 | * 28 | * @var array 29 | */ 30 | protected $fillable = [ 31 | 'token', 'collection_id', 'is_active', 'created_by' 32 | ]; 33 | 34 | /** 35 | * The attributes excluded from the model's JSON form. 36 | * 37 | * @var array 38 | */ 39 | protected $hidden = [ 40 | '', 41 | ]; 42 | } 43 | -------------------------------------------------------------------------------- /app/Models/Tag.php: -------------------------------------------------------------------------------- 1 | 'integer', // because of SQLite 21 | ]; 22 | 23 | /** 24 | * The attributes that are mass assignable. 25 | * 26 | * @var array 27 | */ 28 | protected $fillable = [ 29 | 'name', 'user_id' 30 | ]; 31 | 32 | /** 33 | * The attributes excluded from the model's JSON form. 34 | * 35 | * @var array 36 | */ 37 | protected $hidden = [ 38 | 'user_id', 'deleted_at', 'pivot' 39 | ]; 40 | 41 | /** 42 | * Posts that belong to the tag. 43 | */ 44 | public function posts() 45 | { 46 | return $this->belongsToMany(Post::class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | protected $fillable = [ 31 | 'name', 32 | 'email', 33 | 'password', 34 | ]; 35 | 36 | /** 37 | * The attributes that should be hidden for serialization. 38 | * 39 | * @var array 40 | */ 41 | protected $hidden = [ 42 | 'password', 43 | //'remember_token', 44 | ]; 45 | 46 | /** 47 | * The attributes that should be cast. 48 | * 49 | * @var array 50 | */ 51 | protected $casts = [ 52 | // 'email_verified_at' => 'datetime', 53 | 'permission' => 'integer', // because of SQLite 54 | ]; 55 | 56 | public function sendPasswordResetNotification($token) 57 | { 58 | $this->notify(new \App\Notifications\ResetPassword($token)); 59 | } 60 | 61 | /** 62 | * Get the identifier that will be stored in the subject claim of the JWT. 63 | * 64 | * @return mixed 65 | */ 66 | public function getJWTIdentifier() 67 | { 68 | return $this->getKey(); 69 | } 70 | 71 | /** 72 | * Return a key value array, containing any custom claims to be added to the JWT. 73 | * 74 | * @return array 75 | */ 76 | public function getJWTCustomClaims() 77 | { 78 | return []; 79 | } 80 | 81 | public static function resetUrl() 82 | { 83 | return '/reset'; 84 | } 85 | 86 | public static function getAuthenticationType(): int 87 | { 88 | if (Auth::guard('api')->check()) { 89 | return self::API_USER; 90 | } else if (Auth::guard('share')->check()) { 91 | return self::SHARE_USER; 92 | } 93 | return self::UNAUTHORIZED_USER; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/Notifications/ResetPassword.php: -------------------------------------------------------------------------------- 1 | token = $token; 27 | } 28 | 29 | /** 30 | * Get the notification's delivery channels. 31 | * 32 | * @param mixed $notifiable 33 | * @return array 34 | */ 35 | public function via($notifiable) 36 | { 37 | return ['mail']; 38 | } 39 | 40 | /** 41 | * Get the mail representation of the notification. 42 | * 43 | * @param mixed $notifiable 44 | * @return \Illuminate\Notifications\Messages\MailMessage 45 | */ 46 | public function toMail($notifiable) 47 | { 48 | return (new MailMessage) 49 | ->subject(Lang::get('Reset Password Notification')) 50 | ->line(Lang::get('You are receiving this email because we received a password reset request for your account.')) 51 | ->action( 52 | Lang::get('Reset Password'), 53 | url(User::resetUrl() . '?' . http_build_query([ 54 | 'token' => $this->token, 55 | 'email' => $notifiable->getEmailForPasswordReset() 56 | ]), false) 57 | ) 58 | ->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')])) 59 | ->line(Lang::get('If you did not request a password reset, no further action is required.')); 60 | } 61 | 62 | /** 63 | * Get the array representation of the notification. 64 | * 65 | * @param mixed $notifiable 66 | * @return array 67 | */ 68 | public function toArray($notifiable) 69 | { 70 | return [ 71 | // 72 | ]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Policies/CollectionPolicy.php: -------------------------------------------------------------------------------- 1 | id === $collection->user_id; 33 | } 34 | 35 | /** 36 | * Determine whether the user can update the collection. 37 | * 38 | * @param \App\Models\User $user 39 | * @param \App\Models\Collection $collection 40 | * @return mixed 41 | */ 42 | public function update(User $user, Collection $collection) 43 | { 44 | return $user->id === $collection->user_id; 45 | } 46 | 47 | /** 48 | * Determine whether the user can delete the collection. 49 | * 50 | * @param \App\Models\User $user 51 | * @param \App\Models\Collection $collection 52 | * @return mixed 53 | */ 54 | public function delete(User $user, Collection $collection) 55 | { 56 | return $user->id === $collection->user_id; 57 | } 58 | 59 | /** 60 | * Determine whether the user can inherit the collection. 61 | * 62 | * @param \App\Models\User $user 63 | * @param \App\Models\Collection $collection 64 | * @return mixed 65 | */ 66 | public function inherit(User $user, Collection $collection) 67 | { 68 | return $user->id === $collection->user_id; 69 | } 70 | 71 | public function share(User $user, Collection $collection) 72 | { 73 | return $user->id === $collection->user_id; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/Policies/PostPolicy.php: -------------------------------------------------------------------------------- 1 | id === $post->user_id; 35 | else if ($user instanceof Share) 36 | return $user->collection_id === $post->collection_id; 37 | } 38 | 39 | /** 40 | * Determine whether the user can update the post. 41 | * 42 | * @param \App\Models\User $user 43 | * @param \App\Models\Post $post 44 | * @return mixed 45 | */ 46 | public function update(User $user, Post $post) 47 | { 48 | return $user->id === $post->user_id; 49 | } 50 | 51 | /** 52 | * Determine whether the user can delete the post. 53 | * 54 | * @param \App\Models\User $user 55 | * @param \App\Models\Post $post 56 | * @return mixed 57 | */ 58 | public function delete(User $user, Post $post) 59 | { 60 | return $user->id === $post->user_id; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Policies/PublicSharePolicy.php: -------------------------------------------------------------------------------- 1 | id === $share->created_by; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Policies/TagPolicy.php: -------------------------------------------------------------------------------- 1 | id === $tag->user_id; 33 | } 34 | 35 | /** 36 | * Determine whether the user can update the tag. 37 | * 38 | * @param \App\Models\User $user 39 | * @param \App\Models\Tag $tag 40 | * @return mixed 41 | */ 42 | public function update(User $user, Tag $tag) 43 | { 44 | return $user->id === $tag->user_id; 45 | } 46 | 47 | /** 48 | * Determine whether the user can delete the tag. 49 | * 50 | * @param \App\Models\User $user 51 | * @param \App\Models\Tag $tag 52 | * @return mixed 53 | */ 54 | public function delete(User $user, Tag $tag) 55 | { 56 | return $user->id === $tag->user_id; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Policies/UserPolicy.php: -------------------------------------------------------------------------------- 1 | permission === User::ADMIN; 32 | } 33 | 34 | /** 35 | * Determine whether the user can update the user. 36 | * 37 | * @param \App\Models\User $authUser 38 | * @param \App\Models\User $user 39 | * @return mixed 40 | */ 41 | public function update(User $authUser, User $user) 42 | { 43 | return $authUser->id === $user->id 44 | ? Response::allow() 45 | : Response::deny('Only the user itself can change these information.'); 46 | } 47 | 48 | /** 49 | * Determine whether the user can delete the user. 50 | * 51 | * @param \App\Models\User $authUser 52 | * @return mixed 53 | */ 54 | public function delete(User $authUser) 55 | { 56 | return $authUser->permission === User::ADMIN; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $policies = [ 17 | // 'App\Models\Model' => 'App\Policies\ModelPolicy', 18 | ]; 19 | 20 | /** 21 | * Register any authentication / authorization services. 22 | * 23 | * @return void 24 | */ 25 | public function boot() 26 | { 27 | $this->registerPolicies(); 28 | 29 | $this->app['auth']->viaRequest('api', function ($request) { 30 | return app('auth')->setRequest($request)->user(); 31 | }); 32 | 33 | $this->app['auth']->viaRequest('token', function ($request) { 34 | if ($request->bearerToken()) { 35 | return PublicShare::where('token', $request->bearerToken())->where('is_active', true)->first(); 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | protected $listen = [ 18 | Registered::class => [ 19 | SendEmailVerificationNotification::class, 20 | ], 21 | ]; 22 | 23 | /** 24 | * Register any events for your application. 25 | * 26 | * @return void 27 | */ 28 | public function boot() 29 | { 30 | // 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 39 | 40 | $this->routes(function () { 41 | Route::prefix('api') 42 | ->middleware('api') 43 | ->namespace($this->namespace) 44 | ->group(base_path('routes/api.php')); 45 | 46 | Route::middleware('web') 47 | ->namespace($this->namespace) 48 | ->group(base_path('routes/web.php')); 49 | }); 50 | } 51 | 52 | /** 53 | * Configure the rate limiters for the application. 54 | * 55 | * @return void 56 | */ 57 | protected function configureRateLimiting() 58 | { 59 | RateLimiter::for('api', function (Request $request) { 60 | return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip()); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Services/CollectionService.php: -------------------------------------------------------------------------------- 1 | $name, 16 | 'user_id' => $user_id, 17 | 'parent_id' => $parent_collection_id, 18 | 'icon_id' => $icon_id 19 | ]); 20 | return $collection; 21 | } 22 | 23 | public function update($id, $name, $parent_collection_id, $is_root, $icon_id) 24 | { 25 | $attributes = collect([ 26 | 'name' => $name, 27 | 'icon_id' => $icon_id 28 | ])->filter()->all(); 29 | 30 | if ($is_root) { 31 | $attributes['parent_id'] = null; 32 | } else if (isset($parent_collection_id)) { 33 | $attributes['parent_id'] = $parent_collection_id; 34 | } 35 | 36 | $collection = Collection::find($id); 37 | $collection->update($attributes); 38 | return $collection; 39 | } 40 | 41 | public function delete($id, $is_nested, $user_id) 42 | { 43 | if ($is_nested) { 44 | Collection::where('user_id', $user_id)->where('parent_id', $id)->delete(); 45 | } 46 | Collection::find($id)->delete(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Services/TagService.php: -------------------------------------------------------------------------------- 1 | where('user_id', $user_id)->exists()) { 14 | return null; 15 | } 16 | 17 | $tag = Tag::create([ 18 | 'name' => $name, 19 | 'user_id' => $user_id 20 | ]); 21 | 22 | return $tag; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fr0tt/benotes", 3 | "description": "App for bookmarks and notes.", 4 | "keywords": [ 5 | "laravel", 6 | "bookmark", 7 | "bookmarks", 8 | "notes", 9 | "posts", 10 | "todo" 11 | ], 12 | "license": "MIT", 13 | "version": "2.8.1", 14 | "type": "project", 15 | "require": { 16 | "php": "^7.3|^8.0", 17 | "chrome-php/chrome": "^1.9", 18 | "doctrine/dbal": "^3.5", 19 | "fruitcake/laravel-cors": "^2.0", 20 | "guzzlehttp/guzzle": "^7.0.1", 21 | "intervention/image": "^2.7", 22 | "ksubileau/color-thief-php": "^2.0", 23 | "laravel/framework": "^8.75", 24 | "laravel/tinker": "^2.5", 25 | "league/flysystem-aws-s3-v3": "^1.0", 26 | "php-open-source-saver/jwt-auth": "^2.0", 27 | "spatie/db-dumper": "^3.4", 28 | "spatie/temporary-directory": "^1.3" 29 | }, 30 | "require-dev": { 31 | "facade/ignition": "^2.5", 32 | "fakerphp/faker": "^1.9.1", 33 | "mockery/mockery": "^1.4.4", 34 | "nunomaduro/collision": "^5.10", 35 | "phpunit/phpunit": "^9.5.10" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "App\\": "app/", 40 | "Database\\Factories\\": "database/factories/", 41 | "Database\\Seeders\\": "database/seeders/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Tests\\": "tests/" 47 | } 48 | }, 49 | "scripts": { 50 | "post-autoload-dump": [ 51 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 52 | "@php artisan package:discover --ansi" 53 | ], 54 | "post-update-cmd": [ 55 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 56 | ], 57 | "post-root-package-install": [ 58 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 59 | ], 60 | "post-create-project-cmd": [ 61 | "@php artisan key:generate --ansi" 62 | ] 63 | }, 64 | "extra": { 65 | "laravel": { 66 | "dont-discover": [] 67 | } 68 | }, 69 | "config": { 70 | "optimize-autoloader": true, 71 | "preferred-install": "dist", 72 | "sort-packages": true 73 | }, 74 | "minimum-stability": "dev", 75 | "prefer-stable": true 76 | } 77 | -------------------------------------------------------------------------------- /config/benotes.php: -------------------------------------------------------------------------------- 1 | env('USE_FILESYSTEM', true), 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Curl Timeout 19 | |-------------------------------------------------------------------------- 20 | | 21 | | This option controls the maximum number of seconds to allow cURL functions to execute. 22 | | 23 | */ 24 | 25 | 'curl_timeout' => env('CURL_TIMEOUT', 10), 26 | 27 | 'run_backup' => env('RUN_BACKUP', false), 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Backup Disk 32 | |-------------------------------------------------------------------------- 33 | | 34 | | This option controls which filesystem disk to use. 35 | | 36 | */ 37 | 38 | 'backup_disk' => env('BACKUP_DISK', 'backup'), 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Backup include env 43 | |-------------------------------------------------------------------------- 44 | | 45 | | This option controls if backups should include your .env file 46 | | 47 | */ 48 | 49 | 'backup_include_env' => env('BACKUP_INCLUDE_ENV', false), 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Backup Interval 54 | |-------------------------------------------------------------------------- 55 | | 56 | | This option controls how often a backup should be created. 57 | | 58 | */ 59 | 60 | 'backup_interval' => env('BACKUP_INTERVAL', '0 1 * * *'), 61 | 62 | 'temporary_directory' => storage_path('tmp'), 63 | 64 | 'generate_missing_thumbnails' => env('GENERATE_MISSING_THUMBNAILS', false), 65 | 66 | /* 67 | |-------------------------------------------------------------------------- 68 | | Missing Thumbnail Generation Interval 69 | |-------------------------------------------------------------------------- 70 | | 71 | | This option controls how often missing thumbnails should be created. 72 | | Default is set to every 6 hours. 73 | | 74 | */ 75 | 76 | 'thumbnail_filler_interval' => env('THUMBNAIL_FILLER_INTERVAL', '0 */2 * * *'), 77 | 78 | 'browser' => env('BROWSER', 'chromium') 79 | 80 | ]; 81 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'cluster' => env('PUSHER_APP_CLUSTER'), 40 | 'useTLS' => true, 41 | ], 42 | ], 43 | 44 | 'ably' => [ 45 | 'driver' => 'ably', 46 | 'key' => env('ABLY_KEY'), 47 | ], 48 | 49 | 'redis' => [ 50 | 'driver' => 'redis', 51 | 'connection' => 'default', 52 | ], 53 | 54 | 'log' => [ 55 | 'driver' => 'log', 56 | ], 57 | 58 | 'null' => [ 59 | 'driver' => 'null', 60 | ], 61 | 62 | ], 63 | 64 | ]; 65 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DRIVER', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure as many filesystem "disks" as you wish, and you 24 | | may even configure multiple disks of the same driver. Defaults have 25 | | been setup for each driver as an example of the required options. 26 | | 27 | | Supported Drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app/public'), 36 | ], 37 | 38 | 'public' => [ 39 | 'driver' => 'local', 40 | 'root' => storage_path('app/public'), 41 | 'url' => env('APP_URL') . '/storage', 42 | 'visibility' => 'public', 43 | ], 44 | 45 | 'backup' => [ 46 | 'driver' => 'local', 47 | 'root' => storage_path('backup'), 48 | ], 49 | 50 | 's3' => [ 51 | 'driver' => 's3', 52 | 'key' => env('AWS_ACCESS_KEY_ID'), 53 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 54 | 'region' => env('AWS_DEFAULT_REGION'), 55 | 'bucket' => env('AWS_BUCKET'), 56 | 'url' => env('AWS_URL'), 57 | 'endpoint' => env('AWS_ENDPOINT'), 58 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 59 | ], 60 | 61 | ], 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | Symbolic Links 66 | |-------------------------------------------------------------------------- 67 | | 68 | | Here you may configure the symbolic links that will be created when the 69 | | `storage:link` Artisan command is executed. The array keys should be 70 | | the locations of the links and the values should be their targets. 71 | | 72 | */ 73 | 74 | 'links' => [ 75 | public_path('storage') => storage_path('app/public'), 76 | ], 77 | 78 | ]; 79 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 65536, 48 | 'threads' => 1, 49 | 'time' => 4, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /config/image.php: -------------------------------------------------------------------------------- 1 | env('IMAGE_DRIVER', 'gd') 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /config/sanctum.php: -------------------------------------------------------------------------------- 1 | explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( 17 | '%s%s', 18 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', 19 | env('APP_URL') ? ',' . parse_url(env('APP_URL'), PHP_URL_HOST) : '' 20 | ))), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Sanctum Guards 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This array contains the authentication guards that will be checked when 28 | | Sanctum is trying to authenticate a request. If none of these guards 29 | | are able to authenticate the request, Sanctum will use the bearer 30 | | token that's present on an incoming request for authentication. 31 | | 32 | */ 33 | 34 | 'guard' => ['web'], 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Expiration Minutes 39 | |-------------------------------------------------------------------------- 40 | | 41 | | This value controls the number of minutes until an issued token will be 42 | | considered expired. If this value is null, personal access tokens do 43 | | not expire. This won't tweak the lifetime of first-party sessions. 44 | | 45 | */ 46 | 47 | 'expiration' => env('JWT_REFRESH_TTL', 20160), 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Sanctum Middleware 52 | |-------------------------------------------------------------------------- 53 | | 54 | | When authenticating your first-party SPA with Sanctum you may need to 55 | | customize some of the middleware Sanctum uses while processing the 56 | | request. You may change the middleware listed below as required. 57 | | 58 | */ 59 | 60 | 'middleware' => [ 61 | 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, 62 | 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, 63 | ], 64 | 65 | ]; 66 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | ], 22 | 23 | 'postmark' => [ 24 | 'token' => env('POSTMARK_TOKEN'), 25 | ], 26 | 27 | 'ses' => [ 28 | 'key' => env('AWS_ACCESS_KEY_ID'), 29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 30 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 31 | ], 32 | 33 | ]; 34 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/CollectionFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->company(), 19 | 'user_id' => User::first()->id, 20 | 'icon_id' => $this->faker->numberBetween(101, 108), 21 | 'parent_id' => null 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/factories/PostFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->randomNumber(), 21 | 'title' => $this->faker->title(), 22 | 'content' => $this->faker->sentence(), 23 | 'collection_id' => (Collection::count() > 0) ? Collection::first()->id : null, 24 | 'type' => Post::POST_TYPE_TEXT, 25 | 'user_id' => User::first()->id, 26 | 'order' => Post::where('collection_id', null)->count() 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/factories/TagFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->word(), 19 | 'user_id' => User::first()->id 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name(), 20 | 'email' => $this->faker->unique()->safeEmail(), 21 | //'email_verified_at' => now(), 22 | 'password' => Hash::make($this->faker->password()), 23 | //'remember_token' => Str::random(10), 24 | 'permission' => 7, 25 | ]; 26 | } 27 | 28 | /** 29 | * Indicate that the model's email address should be unverified. 30 | * 31 | * @return \Illuminate\Database\Eloquent\Factories\Factory 32 | */ 33 | public function unverified() 34 | { 35 | return $this->state(function (array $attributes) { 36 | return [ 37 | 'email_verified_at' => null, 38 | ]; 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->timestamp('email_verified_at')->nullable(); 21 | $table->string('password'); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('users'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 18 | $table->string('token'); 19 | $table->timestamp('created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('password_resets'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2019_08_28_093831_create_collections_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->unsignedInteger('user_id'); 20 | $table->foreign('user_id')->references('id')->on('users'); 21 | $table->softDeletes(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('collections'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2020_02_29_231807_create_posts_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->text('content'); 19 | $table->tinyInteger('type'); 20 | $table->text('url')->nullable(); 21 | $table->string('title')->nullable(); 22 | $table->text('description')->nullable(); 23 | $table->string('color', 7)->nullable(); 24 | $table->string('image_path')->nullable(); 25 | $table->string('base_url')->nullable(); 26 | $table->unsignedInteger('collection_id')->nullable(); 27 | $table->foreign('collection_id')->references('id')->on('collections'); 28 | $table->unsignedInteger('user_id'); 29 | $table->foreign('user_id')->references('id')->on('users'); 30 | $table->unsignedSmallInteger('order'); 31 | $table->timestamps(); 32 | $table->softDeletes(); 33 | }); 34 | } 35 | 36 | /** 37 | * Reverse the migrations. 38 | * 39 | * @return void 40 | */ 41 | public function down() 42 | { 43 | Schema::dropIfExists('posts'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /database/migrations/2020_03_04_132210_remove_unused_columns_and_add_permissions_to_users.php: -------------------------------------------------------------------------------- 1 | dropColumn('email_verified_at'); 18 | }); 19 | 20 | if (env('DB_CONNECTION') === 'sqlite') { 21 | // SQLite does not support adding a column to an existing table 22 | // with a NOT NULL constraint without a default value 23 | Schema::table('users', function (Blueprint $table) { 24 | $table->unsignedTinyInteger('permission')->default(0); 25 | }); 26 | } else { 27 | Schema::table('users', function (Blueprint $table) { 28 | $table->unsignedTinyInteger('permission'); 29 | }); 30 | } 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function down() 39 | { 40 | Schema::table('users', function (Blueprint $table) { 41 | // 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2020_03_23_150126_create_shares_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->text('token'); 19 | $table->unsignedInteger('collection_id')->nullable(); 20 | $table->foreign('collection_id')->references('id')->on('collections'); 21 | $table->unsignedBigInteger('post_id')->nullable(); 22 | $table->foreign('post_id')->references('id')->on('posts'); 23 | $table->unsignedTinyInteger('permission')->default(4); 24 | $table->foreign('created_by')->references('id')->on('users'); 25 | $table->unsignedInteger('created_by'); 26 | $table->boolean('is_active'); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('shares'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2020_12_20_133609_change_column_type_from_posts.php: -------------------------------------------------------------------------------- 1 | string('url', 512)->change(); 18 | }); 19 | 20 | // for sqlite 21 | Schema::table('posts', function (Blueprint $table) { 22 | $table->string('image_path', 512)->change(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::table('posts', function (Blueprint $table) { 34 | // 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2021_07_24_224526_change_color_from_posts.php: -------------------------------------------------------------------------------- 1 | string('color', 40)->change(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('posts', function (Blueprint $table) { 29 | // 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2022_03_04_162953_add_icon_to_collections_table.php: -------------------------------------------------------------------------------- 1 | unsignedSmallInteger('icon_id')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('collections', function (Blueprint $table) { 29 | // 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2022_07_18_233713_create_tags_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->unsignedInteger('user_id'); 20 | $table->foreign('user_id')->references('id')->on('users'); 21 | $table->softDeletes(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('tags'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2022_07_18_234358_create_post_tag_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->unsignedBigInteger('post_id')->nullable(); 19 | $table->foreign('post_id')->references('id')->on('posts'); 20 | $table->unsignedBigInteger('tag_id')->nullable(); 21 | $table->foreign('tag_id')->references('id')->on('tags'); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('post_tag'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2022_12_12_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('uuid')->unique(); 19 | $table->text('connection'); 20 | $table->text('queue'); 21 | $table->longText('payload'); 22 | $table->longText('exception'); 23 | $table->timestamp('failed_at')->useCurrent(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('failed_jobs'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2022_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->morphs('tokenable'); 19 | $table->string('name'); 20 | $table->string('token', 64)->unique(); 21 | $table->text('abilities')->nullable(); 22 | $table->timestamp('last_used_at')->nullable(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('personal_access_tokens'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2023_01_28_103727_add_parent_id_to_collections_table.php: -------------------------------------------------------------------------------- 1 | unsignedInteger('parent_id')->nullable(); 18 | $table->foreign('parent_id')->references('id')->on('collections'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('collections', function (Blueprint $table) { 30 | // 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2023_02_26_103159_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('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 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('jobs'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | services: 3 | app: 4 | container_name: app 5 | build: 6 | args: 7 | - USE_COMPOSER=${USE_COMPOSER} 8 | - INSTALL_NODE=${INSTALL_NODE} 9 | context: ./ 10 | restart: unless-stopped 11 | environment: 12 | DB_CONNECTION: ${DB_CONNECTION} 13 | RUN_MIGRATIONS: ${RUN_MIGRATIONS} 14 | ports: 15 | - ${APP_PORT}:80 16 | volumes: 17 | - ./:/var/www 18 | #- benotes_storage:/var/www/storage 19 | - ./docker/data/nginx/logs/:/var/lib/nginx/logs/ 20 | networks: 21 | - benotes 22 | 23 | db: 24 | container_name: db 25 | image: mysql:5.7 26 | restart: unless-stopped 27 | environment: 28 | MYSQL_DATABASE: ${DB_DATABASE} 29 | MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} 30 | MYSQL_USER: ${DB_USERNAME} 31 | MYSQL_PASSWORD: ${DB_PASSWORD} 32 | ports: 33 | - 3306 34 | volumes: 35 | - benotes_mysql:/var/lib/mysql 36 | networks: 37 | - benotes 38 | 39 | networks: 40 | benotes: 41 | driver: bridge 42 | 43 | volumes: 44 | benotes_mysql: 45 | driver: "local" 46 | #benotes_storage: 47 | #driver: "local" -------------------------------------------------------------------------------- /docker/crontab: -------------------------------------------------------------------------------- 1 | 0 */1 * * * php /var/www/artisan schedule:run >> /dev/null 2>&1 2 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -f /var/www/storage/database.sqlite ] && [ "${DB_CONNECTION}" = "sqlite" ]; then 4 | touch /var/www/storage/database.sqlite && chown application:www-data /var/www/storage/database.sqlite 5 | fi 6 | 7 | su-exec application php artisan migrate:retry 8 | 9 | supervisord -c /etc/supervisor.d/supervisord.ini -------------------------------------------------------------------------------- /docker/install.sh: -------------------------------------------------------------------------------- 1 | # How to use 2 | # docker-compose exec app sh 3 | # sh docker/install.sh 4 | 5 | ln -snf ../storage/app/public/ public/storage && \ 6 | composer install --prefer-dist --no-interaction && \ 7 | php artisan install 8 | -------------------------------------------------------------------------------- /docker/nginx-laravel.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | root /var/www/public; 5 | 6 | add_header X-Frame-Options "SAMEORIGIN"; 7 | add_header X-Content-Type-Options "nosniff"; 8 | 9 | index index.php; 10 | 11 | charset utf-8; 12 | 13 | location / { 14 | try_files $uri $uri/ /index.php?$query_string; 15 | } 16 | 17 | location = /favicon.ico { access_log off; log_not_found off; } 18 | location = /robots.txt { access_log off; log_not_found off; } 19 | 20 | error_page 404 /index.php; 21 | 22 | location ~ \.php$ { 23 | fastcgi_pass localhost:9000; 24 | fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; 25 | include fastcgi_params; 26 | } 27 | 28 | location ~ /\.(?!well-known).* { 29 | deny all; 30 | } 31 | } -------------------------------------------------------------------------------- /docker/supervisord.ini: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | logfile=/var/log/supervisord.log 4 | pidfile=/var/run/supervisord.pid 5 | 6 | [program:nginx] 7 | command=nginx 8 | stdout_logfile=/dev/stdout 9 | stdout_logfile_maxbytes=0 10 | stderr_logfile=/dev/stderr 11 | stderr_logfile_maxbytes=0 12 | 13 | [program:php-fpm] 14 | command=php-fpm 15 | stdout_logfile=/dev/stdout 16 | stdout_logfile_maxbytes=0 17 | stderr_logfile=/dev/stderr 18 | stderr_logfile_maxbytes=0 19 | 20 | [program:cron] 21 | command=/usr/sbin/crond -f 22 | autostart=true 23 | autorestart=true 24 | -------------------------------------------------------------------------------- /docker/update.sh: -------------------------------------------------------------------------------- 1 | # How to use 2 | # docker-compose exec app sh 3 | # sh docker/update.sh 4 | 5 | composer install --prefer-dist --no-interaction && \ 6 | php artisan migrate 7 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | if ($http_x_forwarded_proto != 'https') { 2 | rewrite ^ https://$host$request_uri? permanent; 3 | } 4 | 5 | location / { 6 | try_files $uri @rewriteapp; 7 | } 8 | location @rewriteapp { 9 | rewrite ^(.*)$ /index.php$1 last; 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "mix", 6 | "watch": "mix watch", 7 | "watch-poll": "mix watch -- --watch-options-poll=1000", 8 | "hot": "mix watch --hot", 9 | "prod": "npm run production", 10 | "production": "mix --production", 11 | "svg-sprite": "svg-sprite -s --symbol-dest public --symbol-sprite glyphs.svg notes/glyphs/*.svg", 12 | "eslint": "npx eslint . --ext .js,.vue", 13 | "playwright": "npx playwright test", 14 | "playwright-debug": "npx playwright test --project=chromium --debug", 15 | "playwright-trace": "npx playwright test --project=chromium --trace on", 16 | "codegen": "npx playwright codegen" 17 | }, 18 | "devDependencies": { 19 | "@playwright/test": "^1.31.2", 20 | "axios": "^0.21.4", 21 | "browser-sync": "^2.26.14", 22 | "browser-sync-webpack-plugin": "^2.3.0", 23 | "eslint": "^7.32.0", 24 | "eslint-config-prettier": "^8.5.0", 25 | "eslint-plugin-prettier": "^4.2.1", 26 | "eslint-plugin-vue": "^9.3.0", 27 | "laravel-mix": "^6.0.41", 28 | "postcss-loader": "^5.2.0", 29 | "prettier": "^2.7.1", 30 | "resolve-url-loader": "^3.1.2", 31 | "sass": "^1.32.11", 32 | "sass-loader": "^11.0.1", 33 | "svg-vue3": "^0.2.1", 34 | "vue": "^2.7.0", 35 | "vue-loader": "^15.9.6", 36 | "vue-template-compiler": "^2.7.14", 37 | "webpack": "^5.35.0" 38 | }, 39 | "dependencies": { 40 | "@riophae/vue-treeselect": "^0.4.0", 41 | "@tiptap/core": "^2.0.4", 42 | "@tiptap/extension-image": "^2.0.4", 43 | "@tiptap/extension-link": "^2.0.4", 44 | "@tiptap/extension-placeholder": "^2.0.4", 45 | "@tiptap/extension-task-item": "^2.0.4", 46 | "@tiptap/extension-task-list": "^2.0.4", 47 | "@tiptap/extension-typography": "^2.0.4", 48 | "@tiptap/extension-underline": "^2.0.4", 49 | "@tiptap/starter-kit": "^2.0.4", 50 | "@tiptap/vue-2": "^2.0.4", 51 | "dotenv": "^8.2.0", 52 | "laravel-mix-svg-vue": "^0.3.6", 53 | "tailwindcss": "^2.2.16", 54 | "vue-cookie": "^1.1.4", 55 | "vue-lazyload": "^1.3.3", 56 | "vue-router": "^3.4.9", 57 | "vue-select": "^3.20.0", 58 | "vuedraggable": "^2.24.3", 59 | "vuex": "^3.6.0", 60 | "workbox-cacheable-response": "^6.0.2", 61 | "workbox-core": "^6.0.2", 62 | "workbox-expiration": "^6.0.2", 63 | "workbox-precaching": "^6.0.2", 64 | "workbox-routing": "^6.0.2", 65 | "workbox-strategies": "^6.0.2", 66 | "workbox-webpack-plugin": "^6.0.2" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /playwright/beforeEach.js: -------------------------------------------------------------------------------- 1 | import { expect } from '@playwright/test' 2 | 3 | const beforeEach = async ({ page, request }) => { 4 | let response = null 5 | 6 | const user = { 7 | email: Date.now() + 'test@benotes.org', 8 | password: 'password', 9 | } 10 | 11 | response = await request.post('/api/__e2e__/user', { 12 | data: user, 13 | }) 14 | await expect(response.ok()).toBeTruthy() 15 | 16 | await page.context().clearCookies() 17 | 18 | await page.goto('/login') 19 | await page.getByPlaceholder('Email Address').fill(user.email) 20 | await page.getByPlaceholder('Password').fill(user.password) 21 | await page.getByRole('button', { name: 'Login' }).click() 22 | 23 | //await page.waitForNavigation() // should work without it 24 | } 25 | 26 | export default beforeEach 27 | -------------------------------------------------------------------------------- /playwright/global-setup.js: -------------------------------------------------------------------------------- 1 | const { chromium, request, expect } = require('@playwright/test') 2 | const fs = require('fs') 3 | 4 | module.exports = async (config) => { 5 | const { baseURL, storageState } = config.projects[0].use 6 | 7 | if (fs.existsSync('.env.playwright')) { 8 | fs.renameSync('.env', '.env.original') 9 | fs.renameSync('.env.playwright', '.env') 10 | } 11 | 12 | if (!fs.existsSync(storageState)) { 13 | fs.writeFileSync(storageState, JSON.stringify({})) 14 | } 15 | 16 | const requestContext = await request.newContext({ 17 | baseURL: baseURL, 18 | }) 19 | 20 | let response = await requestContext.post('/api/__e2e__/setup') 21 | await expect(response.ok()).toBeTruthy() 22 | } 23 | -------------------------------------------------------------------------------- /playwright/global-teardown.js: -------------------------------------------------------------------------------- 1 | const { request } = require('@playwright/test') 2 | const fs = require('fs') 3 | 4 | module.exports = async (config) => { 5 | const { baseURL } = config.projects[0].use 6 | if (fs.existsSync('.env.original')) { 7 | fs.renameSync('.env', '.env.playwright') 8 | fs.renameSync('.env.original', '.env') 9 | } 10 | 11 | const requestContext = await request.newContext({ 12 | baseURL: baseURL, 13 | }) 14 | 15 | await requestContext.post(`${baseURL}/__e2e__/teardown`) 16 | } 17 | -------------------------------------------------------------------------------- /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/fonts/Inter-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Black.woff -------------------------------------------------------------------------------- /public/fonts/Inter-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Black.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-BlackItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter-BlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-BlackItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Bold.woff -------------------------------------------------------------------------------- /public/fonts/Inter-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-BoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-BoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-ExtraBold.woff -------------------------------------------------------------------------------- /public/fonts/Inter-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-ExtraBold.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-ExtraBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-ExtraBoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter-ExtraBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-ExtraBoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-ExtraLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-ExtraLight.woff -------------------------------------------------------------------------------- /public/fonts/Inter-ExtraLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-ExtraLight.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-ExtraLightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-ExtraLightItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter-ExtraLightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-ExtraLightItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Italic.woff -------------------------------------------------------------------------------- /public/fonts/Inter-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Italic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Light.woff -------------------------------------------------------------------------------- /public/fonts/Inter-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Light.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-LightItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-LightItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Medium.woff -------------------------------------------------------------------------------- /public/fonts/Inter-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-MediumItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-MediumItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Regular.woff -------------------------------------------------------------------------------- /public/fonts/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Regular.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-SemiBold.woff -------------------------------------------------------------------------------- /public/fonts/Inter-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-SemiBold.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-SemiBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-SemiBoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter-SemiBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-SemiBoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Thin.woff -------------------------------------------------------------------------------- /public/fonts/Inter-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-Thin.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-ThinItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-ThinItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter-ThinItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-ThinItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-italic.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-italic.var.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/fonts/Inter.var.woff2 -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /public/js/app.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Vue-Lazyload.js v1.3.5 3 | * (c) 2023 Awe 4 | * Released under the MIT License. 5 | */ 6 | 7 | /*! 8 | * Vue.js v2.7.15 9 | * (c) 2014-2023 Evan You 10 | * Released under the MIT License. 11 | */ 12 | 13 | /*! 14 | * tiny-cookie - A tiny cookie manipulation plugin 15 | * https://github.com/Alex1990/tiny-cookie 16 | * Under the MIT license | (c) Alex Chao 17 | */ 18 | 19 | /*! 20 | * vue-treeselect v0.4.0 | (c) 2017-2019 Riophae Lee 21 | * Released under the MIT License. 22 | * https://vue-treeselect.js.org/ 23 | */ 24 | 25 | /*! 26 | * vuex v3.6.2 27 | * (c) 2021 Evan You 28 | * @license MIT 29 | */ 30 | 31 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 32 | 33 | /**! 34 | * Sortable 1.10.2 35 | * @author RubaXa 36 | * @author owenm 37 | * @license MIT 38 | */ 39 | -------------------------------------------------------------------------------- /public/logo_144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/public/logo_144x144.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Benotes", 3 | "short_name": "Benotes", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "orientation": "natural", 7 | "background_color": "#ffffff", 8 | "description": "A note taking and bookmarks collecting app", 9 | "theme_color": "#FF7700", 10 | "icons": [ 11 | { 12 | "src": "./logo_144x144.png", 13 | "sizes": "144x144", 14 | "type": "image/png" 15 | } 16 | ], 17 | "share_target": { 18 | "action": "/c/0/p/create", 19 | "method": "GET", 20 | "params": { 21 | "title": "title", 22 | "text": "text", 23 | "url": "url" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/app.js": "/js/app.js?id=5234050aa620b3e5aebe60e951bedfdd", 3 | "/css/app.css": "/css/app.css?id=c96f213a8fd919bdba321ca0a4bd8bd0" 4 | } 5 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/js/api/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import store from './../store' 3 | import Vue from 'vue' 4 | 5 | /** 6 | * @param {int} collection_id Should already be parsed by parseCollectionId() 7 | * @param {string} filter 8 | * @param {boolean} isArchived 9 | * @param {int} limit 10 | * @param {int} after_id 11 | */ 12 | function refresh() { 13 | return new Promise((resolve, reject) => { 14 | axios 15 | .post('/api/auth/refresh') 16 | .then((response) => { 17 | const token = response.data.data.token.access_token 18 | Vue.cookie.set('token', token, { expires: 14, samesite: 'Strict' }) 19 | axios.defaults.headers.common = { Authorization: `Bearer ${token}` } 20 | axios 21 | .get('/api/auth/me') 22 | .then((response) => { 23 | const user = response.data.data 24 | store.commit('auth/setAuthUser', user) 25 | store.commit('auth/setStaticAuth', null) 26 | resolve(response) 27 | }) 28 | .catch((error) => { 29 | reject(error) 30 | }) 31 | }) 32 | .catch((error) => { 33 | reject(error) 34 | }) 35 | }) 36 | } 37 | 38 | export { refresh } 39 | -------------------------------------------------------------------------------- /resources/js/api/collection.js: -------------------------------------------------------------------------------- 1 | import store from './../store' 2 | 3 | export function getCollectionName(collectionId) { 4 | if (collectionId === null) { 5 | return 'Uncategorized' 6 | } 7 | if (store.state.collection.collections == null) { 8 | return '' 9 | } 10 | const collection = store.state.collection.collections.find((collection) => { 11 | return collection.id === collectionId 12 | }) 13 | if (collection == null) { 14 | return '' 15 | } 16 | return collection.name 17 | } 18 | 19 | export function collectionIconIsInline(iconId) { 20 | return [4003, 4008, 4010, 4017, 4103].indexOf(iconId) > -1 21 | } 22 | -------------------------------------------------------------------------------- /resources/js/api/post.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import store from './../store' 3 | 4 | /** 5 | * @param {int} collection_id Should already be parsed by parseCollectionId() 6 | * @param {int} tag_id 7 | * @param {string} filter 8 | * @param {boolean} isArchived 9 | * @param {int} limit 10 | * @param {int} after_id 11 | */ 12 | function getPosts( 13 | collection_id, 14 | tag_id = null, 15 | filter = null, 16 | isArchived = false, 17 | limit = 0, 18 | after_id = null, 19 | withTags = false 20 | ) { 21 | return axios.get('/api/posts', { 22 | params: { 23 | collection_id: collection_id, 24 | is_uncategorized: isUncategorized(collection_id), 25 | tag_id: tag_id, 26 | filter: filter, 27 | is_archived: isArchived, 28 | limit: limit, 29 | after_id: after_id, 30 | withTags: withTags, 31 | }, 32 | }) 33 | } 34 | 35 | /** 36 | * @param {Object} post 37 | */ 38 | function restorePost(post) { 39 | store.dispatch('post/updatePost', { post: post, transfer: true, restore: true }) 40 | } 41 | 42 | /** 43 | * @param {int} collection_id 44 | */ 45 | function isUncategorized(id) { 46 | return (id === 0) | 0 47 | } 48 | 49 | /** 50 | * @param {int} collection_id 51 | */ 52 | function parseCollectionId(id) { 53 | return id > 0 ? id : null 54 | } 55 | 56 | export { getPosts, restorePost, isUncategorized, parseCollectionId } 57 | -------------------------------------------------------------------------------- /resources/js/components/App.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 61 | 62 | 73 | -------------------------------------------------------------------------------- /resources/js/components/CollectionSidebar.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 72 | -------------------------------------------------------------------------------- /resources/js/components/Deselect.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/js/components/IconPicker.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 47 | 48 | 56 | -------------------------------------------------------------------------------- /resources/js/components/OpenIndicator.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /resources/js/components/PostItemPlaceholder.vue: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /resources/js/components/PostItemTags.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 43 | -------------------------------------------------------------------------------- /resources/js/components/PostLoader.vue: -------------------------------------------------------------------------------- 1 | 13 | 62 | -------------------------------------------------------------------------------- /resources/js/components/UnfurlingLink.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 72 | 80 | -------------------------------------------------------------------------------- /resources/js/components/pages/EditTag.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 93 | -------------------------------------------------------------------------------- /resources/js/components/pages/ExportBookmarks.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 73 | -------------------------------------------------------------------------------- /resources/js/components/pages/Forgot.vue: -------------------------------------------------------------------------------- 1 | 44 | 76 | 81 | -------------------------------------------------------------------------------- /resources/js/components/pages/Search.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 46 | 84 | -------------------------------------------------------------------------------- /resources/js/components/pages/Users.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 68 | 69 | 85 | -------------------------------------------------------------------------------- /resources/js/service-worker.js: -------------------------------------------------------------------------------- 1 | import { skipWaiting, clientsClaim } from 'workbox-core' 2 | import { precacheAndRoute } from 'workbox-precaching' 3 | import { registerRoute } from 'workbox-routing' 4 | import { CacheFirst, StaleWhileRevalidate, NetworkFirst } from 'workbox-strategies' 5 | import { CacheableResponsePlugin } from 'workbox-cacheable-response' 6 | import { ExpirationPlugin } from 'workbox-expiration' 7 | 8 | precacheAndRoute(self.__WB_MANIFEST) 9 | clientsClaim() 10 | self.skipWaiting() 11 | 12 | registerRoute( 13 | ({ request }) => request.destination === 'image', 14 | new CacheFirst({ 15 | cacheName: 'images', 16 | plugins: [ 17 | new ExpirationPlugin({ 18 | maxEntries: 60, 19 | maxAgeSeconds: 30 * 24 * 3600, 20 | }), 21 | ], 22 | }) 23 | ) 24 | 25 | registerRoute( 26 | ({ request }) => request.destination === 'script' || request.destination === 'style', 27 | new StaleWhileRevalidate({ 28 | cacheName: 'static-resources', 29 | }) 30 | ) 31 | 32 | registerRoute( 33 | new RegExp('/'), 34 | new NetworkFirst({ 35 | cacheName: 'html-content', 36 | plugins: [ 37 | new CacheableResponsePlugin({ 38 | statuses: [0, 200], 39 | }), 40 | new ExpirationPlugin({ 41 | maxEntries: 50, 42 | maxAgeSeconds: 30 * 3600, 43 | }), 44 | ], 45 | }) 46 | ) 47 | -------------------------------------------------------------------------------- /resources/js/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import auth from './modules/auth' 5 | import post from './modules/post' 6 | import collection from './modules/collection' 7 | import appbar from './modules/appbar' 8 | import notification from './modules/notification' 9 | 10 | Vue.use(Vuex) 11 | 12 | export default new Vuex.Store({ 13 | modules: { 14 | auth, 15 | post, 16 | collection, 17 | appbar, 18 | notification, 19 | }, 20 | strict: process.env.NODE_ENV !== 'production', 21 | state: { 22 | isMobile: false, 23 | showSidebar: localStorage.getItem('sidebar') === 'false' ? false : true, 24 | showBottomSheet: false, 25 | bottomSheet: [], 26 | }, 27 | mutations: { 28 | isMobile(state, isMobile) { 29 | state.isMobile = isMobile 30 | }, 31 | showSidebar(state, showSidebar) { 32 | state.showSidebar = showSidebar 33 | }, 34 | showBottomSheet(state, showBottomSheet) { 35 | state.showBottomSheet = showBottomSheet 36 | }, 37 | setBottomSheet(state, bottomSheet) { 38 | state.bottomSheet = bottomSheet 39 | }, 40 | }, 41 | actions: { 42 | toggleSidebar(context) { 43 | const showSidebar = !this.state.showSidebar 44 | context.commit('showSidebar', showSidebar) 45 | localStorage.setItem('sidebar', showSidebar) 46 | }, 47 | hideSidebarOnMobile(context) { 48 | if (this.state.isMobile && this.state.showSidebar) { 49 | context.commit('showSidebar', false) 50 | localStorage.setItem('sidebar', false) 51 | } 52 | }, 53 | }, 54 | }) 55 | -------------------------------------------------------------------------------- /resources/js/store/modules/appbar.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaced: true, 3 | state: { 4 | title: '', 5 | hint: '', 6 | button: { 7 | label: '', 8 | icon: '', 9 | callback: null, 10 | }, 11 | options: [], 12 | }, 13 | mutations: { 14 | setAppbar(state, appbar) { 15 | state.title = appbar.title 16 | state.hint = appbar.hint 17 | if (typeof appbar.button !== 'undefined' && appbar.button) { 18 | state.button = { 19 | label: appbar.button.label, 20 | callback: appbar.button.callback, 21 | icon: appbar.button.icon, 22 | } 23 | } 24 | state.options = appbar.options 25 | }, 26 | setTitle(state, title) { 27 | state.title = title 28 | if (title) { 29 | document.title = 'Benotes - ' + title 30 | } 31 | }, 32 | setOptions(state, options) { 33 | state.options = options 34 | }, 35 | }, 36 | actions: { 37 | setAppbar(context, appbar) { 38 | context.commit('setAppbar', appbar) 39 | if (appbar && appbar.title) { 40 | document.title = 'Benotes - ' + appbar.title 41 | } else if (appbar.title) { 42 | document.title = 'Benotes' 43 | } 44 | }, 45 | setOptions(context, options) { 46 | context.commit('setOptions', options) 47 | }, 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /resources/js/store/modules/notification.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaced: true, 3 | state: { 4 | type: '', 5 | title: '', 6 | description: '', 7 | isVisible: false, 8 | }, 9 | mutations: { 10 | setNotification(state, notification) { 11 | state.type = notification.type 12 | state.title = notification.title 13 | state.description = notification.description 14 | }, 15 | showNotification(state, isVisible) { 16 | state.isVisible = isVisible 17 | }, 18 | }, 19 | actions: { 20 | setNotification(context, notification) { 21 | context.commit('setNotification', notification) 22 | context.commit('showNotification', true) 23 | setTimeout(function () { 24 | context.commit('showNotification', false) 25 | }, 3 * 1000) 26 | }, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /resources/lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /resources/lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /resources/sass/theme.scss: -------------------------------------------------------------------------------- 1 | @import "themes/dark"; -------------------------------------------------------------------------------- /resources/sass/themes/dark.scss: -------------------------------------------------------------------------------- 1 | html.dark { 2 | @apply bg-gray-800 text-white; 3 | .theme__appbar { 4 | @apply bg-gray-800; 5 | } 6 | .theme__appbar__title { 7 | @apply text-gray-100; 8 | } 9 | .theme__appbar__menu_icon { 10 | @apply text-white; 11 | } 12 | .theme__sidebar { 13 | @apply text-white bg-gray-800; 14 | box-shadow: 2px 3px 3px 0 rgb(0 0 0 / 50%) 15 | } 16 | .theme__sidebar__collection.router-link-exact-active-parent, .theme__sidebar__collection.router-link-exact-active { 17 | @apply bg-black border-orange-600 text-gray-300; 18 | } 19 | .theme__sidebar__label { 20 | @apply text-white font-normal; 21 | } 22 | .theme__sidebar__subhead { 23 | @apply text-gray-200; 24 | } 25 | .theme__post_item { 26 | @apply text-white; 27 | background-color: #0f1115; 28 | .description { 29 | @apply text-gray-600; 30 | } 31 | .tags { 32 | background-color: #0f1115; 33 | } 34 | .tag { 35 | @apply text-orange-600 font-light; 36 | } 37 | .favicon { 38 | @apply bg-white; 39 | border-radius: 1rem; 40 | padding: 0.05rem; 41 | } 42 | .more-icon, .restore-icon { 43 | @apply text-gray-400 fill-current; 44 | } 45 | } 46 | .theme__modal { 47 | @apply bg-gray-800; 48 | } 49 | .theme__tags__list_item, .theme__users__list_item, .theme__users__list_item th { 50 | @apply text-white; 51 | } 52 | .theme__users__list_item:hover td { 53 | @apply text-orange-600 bg-gray-900; 54 | } 55 | .theme__tags__list_item:nth-child(even), 56 | .theme__users__list_item:nth-child(even) { 57 | @apply text-black; 58 | } 59 | h1 { 60 | @apply text-gray-100; 61 | } 62 | button.button { 63 | @apply border font-normal; 64 | } 65 | .searchbar { 66 | @apply bg-transparent; 67 | } 68 | input.input { 69 | @apply text-white; 70 | } 71 | input.input:focus { 72 | @apply border-white; 73 | } 74 | .editorContent { 75 | @apply text-white; 76 | } 77 | #iconPicker { 78 | @apply text-black; 79 | } 80 | .vue-treeselect__control { 81 | @apply bg-transparent; 82 | } 83 | .vue-treeselect__single-value { 84 | @apply text-white; 85 | } 86 | .vue-treeselect__menu { 87 | @apply text-black; 88 | } 89 | .vs__dropdown-toggle { 90 | @apply bg-transparent; 91 | } 92 | .vs__dropdown-menu { 93 | @apply text-black; 94 | } 95 | } 96 | 97 | 98 | -------------------------------------------------------------------------------- /resources/svg/arrow_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /resources/svg/bookmark_export.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/svg/glyphs/4003.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /resources/svg/glyphs/4008.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/svg/glyphs/4017.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/svg/logo_64x64.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/svg/material/arrow_drop_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/art_track.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/autorenew.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/check_box.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/clear.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/format_bold.svg: -------------------------------------------------------------------------------- 1 | Bold -------------------------------------------------------------------------------- /resources/svg/material/format_italic.svg: -------------------------------------------------------------------------------- 1 | Italic -------------------------------------------------------------------------------- /resources/svg/material/format_quote.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/format_underlined.svg: -------------------------------------------------------------------------------- 1 | Underline -------------------------------------------------------------------------------- /resources/svg/material/horizontal_rule.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/label.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/list_bulleted.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/list_numbered.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/material/undo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/arrow-down-s-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/folder-3-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/folder-add-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/folder-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/folder-unknow-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/git-repository-commits-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/group-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/inbox-unarchive-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/logout-circle-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/menu-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/more-2-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/refresh-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/search-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/settings-3-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/upload-2-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/remix/user-settings-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/zondicons/add-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/zondicons/add-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/zondicons/checkmark-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/zondicons/paste.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/zondicons/stand-by.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/zondicons/text-decoration.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/svg/zondicons/trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Benotes 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /resources/views/stubs/env.blade.php: -------------------------------------------------------------------------------- 1 | APP_NAME=Benotes 2 | APP_ENV=local 3 | APP_DEBUG=false 4 | APP_URL={{ $app_url }} 5 | 6 | APP_KEY={{ $app_key }} 7 | JWT_SECRET={{ $jwt_secret }} 8 | USE_FILESYSTEM={{ $use_filesystem }} 9 | 10 | DB_CONNECTION={{ $db_connection }} 11 | DB_URL={{ $db_url }} 12 | DB_HOST={{ $db_host }} 13 | DB_PORT={{ $db_port }} 14 | DB_DATABASE={{ $db_database }} 15 | DB_USERNAME={{ $db_username }} 16 | DB_PASSWORD={{ $db_password }} 17 | 18 | MAIL_DRIVER=smtp 19 | MAIL_HOST= 20 | MAIL_PORT=587 21 | MAIL_USERNAME= 22 | MAIL_PASSWORD= 23 | MAIL_ENCRYPTION=tls 24 | MAIL_FROM_ADDRESS= -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | where('any', '^(?!api).*'); // .* 17 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | $uri = urldecode( 11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? '' 12 | ); 13 | 14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 15 | // built-in PHP web server. This provides a convenient way to test a Laravel 16 | // application without having installed a "real" web server software here. 17 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { 18 | return false; 19 | } 20 | 21 | require_once __DIR__.'/public/index.php'; 22 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/thumbnails/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /storage/backup/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/database.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr0tt/benotes/cb803b32340eb7720131a9d0b93261d01800c896/storage/database.sqlite -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /storage/tmp/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: { 3 | content: [ 4 | './resources/**/*.vue', 5 | './resources/js/**/*.js', 6 | './resources/views/**/*.blade.php', 7 | './resources/views/**/*.twig', 8 | ], 9 | safelist: [ 10 | 'text-red-600', 11 | 'text-green-600', 12 | 'border-red-600', 13 | 'border-green-600', 14 | ], 15 | }, 16 | theme: { 17 | extend: { 18 | colors: { 19 | orange: { 20 | 200: '#ffe4cc', 21 | 600: '#FF7700', 22 | // from tailwind v1 23 | 100: '#fffaf0', 24 | 300: '#fbd38d', 25 | 500: '#ed8936', 26 | 700: '#c05621', 27 | }, 28 | gray: { 29 | 100: '#fbfbfb', 30 | 200: '#f3f3f3', 31 | 800: '#1e242d', 32 | 900: '#14181d', 33 | // from tailwind v1 34 | 300: '#e2e8f0', 35 | 400: '#cbd5e0', 36 | 500: '#a0aec0', 37 | 600: '#718096', 38 | 700: '#4a5568', 39 | }, 40 | blue: { 41 | // from tailwind v1 42 | 600: '#3182ce', 43 | }, 44 | }, 45 | fontFamily: { 46 | mono: [ 47 | 'JetBrains Mono', 48 | 'Fira Code', 49 | 'Cascadia Code', 50 | 'Consolas', 51 | 'Courier New', 52 | 'monospace', 53 | ], 54 | }, 55 | spacing: { 56 | 1.5: '0.375rem', 57 | 7: '1.75rem', 58 | 80: '20rem', 59 | }, 60 | }, 61 | }, 62 | variants: {}, 63 | plugins: [], 64 | } 65 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Feature/TagTest.php: -------------------------------------------------------------------------------- 1 | create(); 17 | $tag_name = $this->faker->word(); 18 | 19 | $response = $this->actingAs($user)->json('POST', 'api/tags', [ 20 | 'name' => $tag_name, 21 | ]); 22 | 23 | $this->assertEquals(201, $response->status()); 24 | $data = $response->getData()->data; 25 | $this->assertEquals($tag_name, $data->name); 26 | } 27 | 28 | public function testCreateMultipleTagsAtOnce() 29 | { 30 | $user = User::factory()->create(); 31 | $tags = []; 32 | 33 | for ($i = 0; $i < 4; $i++) { 34 | $tags[] = [ 35 | 'name' => $this->faker->word() 36 | ]; 37 | } 38 | 39 | $response = $this->actingAs($user)->json('POST', 'api/tags', [ 40 | 'tags' => $tags, 41 | ]); 42 | 43 | $this->assertEquals(201, $response->status()); 44 | $data = $response->getData()->data; 45 | $this->assertNotEquals(null, $data); 46 | $this->assertEquals($tags[0]['name'], $data[0]->name); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Feature/UserTest.php: -------------------------------------------------------------------------------- 1 | create([ 21 | 'permission' => 255 22 | ]); 23 | 24 | $response = $this->actingAs($user)->json('POST', 'api/users', [ 25 | 'name' => 'John Smith', 26 | 'email' => 'john@example.com', 27 | 'password' => 'Foo1234bar' 28 | ]); 29 | 30 | $this->assertEquals(201, $response->status()); 31 | $data = $response->getData()->data; 32 | $this->assertNotEquals(null, $data); 33 | } 34 | 35 | public function testCreateUserWithoutAuth() 36 | { 37 | $response = $this->json('POST', 'api/users', [ 38 | 'name' => 'Johnny Smith', 39 | 'email' => 'johnny@example.com', 40 | 'password' => 'Foo1234bar' 41 | ]); 42 | 43 | $this->assertGreaterThan(299, $response->status()); 44 | } 45 | 46 | public function testChangeUserPassword() 47 | { 48 | $user = User::factory()->create([ 49 | 'password' => 'Foo1234bar' 50 | ]); 51 | 52 | $response = $this->actingAs($user)->json('PATCH', 'api/users/' . $user->id, [ 53 | 'password_old' => 'Foo1234bar', 54 | 'password_new' => 'foo1234baR' 55 | ]); 56 | 57 | $this->assertEquals(200, $response->status()); 58 | } 59 | 60 | public function testDeleteUser() 61 | { 62 | $user = User::factory()->create([ 63 | 'permission' => 255 64 | ]); 65 | 66 | $collection = Collection::factory()->create([ 67 | 'user_id' => $user->id 68 | ]); 69 | 70 | Post::factory()->create([ 71 | 'user_id' => $user->id, 72 | 'collection_id' => $collection->id 73 | ]); 74 | 75 | 76 | $user2 = User::factory()->create(); 77 | 78 | $collection2 = Collection::factory()->create([ 79 | 'user_id' => $user2->id 80 | ]); 81 | 82 | Post::factory()->create([ 83 | 'user_id' => $user2->id, 84 | 'collection_id' => $collection2->id 85 | ]); 86 | 87 | $response = $this->actingAs($user)->json('DELETE', 'api/users/' . $user->id); 88 | 89 | $this->assertEquals(200, $response->status()); 90 | 91 | // only data of the deleted user should be deleted, not everything from everyone 92 | $this->assertGreaterThan(0, Post::where('user_id', $user2->id)->count()); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix') 2 | const tailwindcss = require('tailwindcss') 3 | require('laravel-mix-svg-vue') 4 | const { InjectManifest } = require('workbox-webpack-plugin') 5 | 6 | const dotenv = require('dotenv') 7 | dotenv.config() 8 | 9 | /* 10 | |-------------------------------------------------------------------------- 11 | | Mix Asset Management 12 | |-------------------------------------------------------------------------- 13 | | 14 | | Mix provides a clean, fluent API for defining some Webpack build steps 15 | | for your Laravel application. By default, we are compiling the Sass 16 | | file for the application as well as bundling up all the JS files. 17 | | 18 | */ 19 | 20 | mix.disableNotifications() 21 | 22 | /* 23 | 24 | // does not work with current webpack version, results in empty css file in production 25 | 26 | mix.babelConfig({ 27 | plugins: ['@babel/plugin-syntax-dynamic-import'] 28 | }) 29 | 30 | mix.extract() 31 | 32 | */ 33 | 34 | mix.js('resources/js/app.js', 'public/js') 35 | .vue({ version: 2 }) 36 | .sass('resources/sass/app.scss', 'public/css') 37 | .options({ 38 | processCssUrls: false, 39 | postCss: [tailwindcss('./tailwind.config.js')], 40 | }) 41 | .svgVue({ 42 | svgoSettings: [ 43 | { removeTitle: false }, 44 | { removeViewBox: false }, 45 | { removeDimensions: true }, 46 | { cleanupIDs: false }, 47 | ], 48 | }) 49 | .webpackConfig({ 50 | plugins: [ 51 | new InjectManifest({ 52 | swSrc: './resources/js/service-worker.js', 53 | }), 54 | ], 55 | output: { 56 | publicPath: '', 57 | }, 58 | }) 59 | .version() 60 | .browserSync(process.env.APP_URL) 61 | --------------------------------------------------------------------------------