├── .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 |
4 |
5 |
6 | Benotes
7 |
8 |
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 |
2 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
61 |
62 |
73 |
--------------------------------------------------------------------------------
/resources/js/components/CollectionSidebar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
27 |
28 |
29 |
31 |
32 |
33 |
34 |
35 |
36 |
72 |
--------------------------------------------------------------------------------
/resources/js/components/Deselect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/js/components/IconPicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Icons
4 |
5 |
13 |
24 |
25 |
26 |
27 |
28 |
47 |
48 |
56 |
--------------------------------------------------------------------------------
/resources/js/components/OpenIndicator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
--------------------------------------------------------------------------------
/resources/js/components/PostItemPlaceholder.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/resources/js/components/PostItemTags.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
21 |
22 |
43 |
--------------------------------------------------------------------------------
/resources/js/components/PostLoader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
9 | loading...
10 |
11 |
12 |
13 |
62 |
--------------------------------------------------------------------------------
/resources/js/components/UnfurlingLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 | {{ node.attrs['data-title'] }}
13 |
14 |
15 | {{ node.attrs.href }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
72 |
80 |
--------------------------------------------------------------------------------
/resources/js/components/pages/EditTag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
93 |
--------------------------------------------------------------------------------
/resources/js/components/pages/ExportBookmarks.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Export Bookmarks
7 |
8 |
9 |
Export bookmarks from Benotes. You can use them in your favorite
10 | web browser or somewhere else accepting NETSCAPE-Bookmark files.
11 |
12 |
13 |
14 |
15 | This may take a minute.
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
73 |
--------------------------------------------------------------------------------
/resources/js/components/pages/Forgot.vue:
--------------------------------------------------------------------------------
1 |
2 |
43 |
44 |
76 |
81 |
--------------------------------------------------------------------------------
/resources/js/components/pages/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Search
6 |
7 |
8 |
9 |
12 | No search results.
13 |
18 |
19 |
20 |
21 |
22 |
23 |
46 |
84 |
--------------------------------------------------------------------------------
/resources/js/components/pages/Users.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | Name |
11 | Email |
12 | |
13 |
14 |
20 | {{ user.name }} |
21 | {{ user.email }} |
22 |
23 | Owner
28 | |
29 |
30 |
31 |
32 |
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 |
--------------------------------------------------------------------------------
/resources/svg/bookmark_export.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/resources/svg/glyphs/4003.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/svg/glyphs/4008.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/svg/glyphs/4017.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/svg/logo_64x64.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/resources/svg/material/format_italic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/svg/material/format_quote.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/svg/material/format_underlined.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------