├── public
├── favicon.ico
├── robots.txt
├── assets
│ └── favicons
│ │ ├── favicon.ico
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── apple-touch-icon.png
│ │ ├── android-chrome-192x192.png
│ │ └── android-chrome-512x512.png
├── manifest.json
├── .htaccess
└── index.php
├── database
├── .gitignore
├── factories
│ ├── GroupFactory.php
│ ├── LinkFactory.php
│ └── UserFactory.php
├── migrations
│ ├── 2024_02_19_210136_increase_link_column_size.php
│ ├── 2022_11_26_194415_remove_media_type_column_from_links_table.php
│ ├── 2023_03_19_203332_add_expires_at_column_to_personal_access_tokens_table.php
│ ├── 2024_04_05_185606_add_query_options_columns_to_groups_table.php
│ ├── 2024_03_02_180621_create_public_links_tables.php
│ ├── 2019_12_14_000001_create_personal_access_tokens_table.php
│ ├── 2022_08_11_143315_add_two_factor_confirmed_column_to_users_table.php
│ └── 2023_04_30_173655_drop_later_table.php
└── seeders
│ └── DatabaseSeeder.php
├── bootstrap
├── cache
│ └── .gitignore
└── app.php
├── storage
├── logs
│ └── .gitignore
├── app
│ ├── public
│ │ └── .gitignore
│ └── .gitignore
└── framework
│ ├── testing
│ └── .gitignore
│ ├── views
│ └── .gitignore
│ ├── cache
│ ├── data
│ │ └── .gitignore
│ └── .gitignore
│ ├── sessions
│ └── .gitignore
│ └── .gitignore
├── docs
└── images
│ └── home.jpg
├── tests
├── Unit
│ └── ExampleTest.php
├── Feature
│ ├── ExampleTest.php
│ ├── BrowserSessionsTest.php
│ ├── ProfileInformationTest.php
│ ├── DeleteApiTokenTest.php
│ ├── CreateApiTokenTest.php
│ ├── AuthenticationTest.php
│ ├── DeleteAccountTest.php
│ ├── PasswordConfirmationTest.php
│ ├── ApiTokenPermissionsTest.php
│ ├── RegistrationTest.php
│ ├── UpdatePasswordTest.php
│ ├── TwoFactorAuthenticationSettingsTest.php
│ └── EmailVerificationTest.php
├── TestCase.php
├── CreatesApplication.php
└── Pest.php
├── resources
├── markdown
│ ├── policy.md
│ └── terms.md
├── fonts
│ ├── inter
│ │ ├── InterVariable.woff2
│ │ └── InterVariable-Italic.woff2
│ └── jetbrains-mono
│ │ ├── JetBrainsMono-Bold.woff2
│ │ ├── JetBrainsMono-Light.woff2
│ │ ├── JetBrainsMono-Thin.woff2
│ │ ├── JetBrainsMono-Italic.woff2
│ │ ├── JetBrainsMono-Medium.woff2
│ │ ├── JetBrainsMono-Regular.woff2
│ │ ├── JetBrainsMono-SemiBold.woff2
│ │ ├── JetBrainsMono-BoldItalic.woff2
│ │ ├── JetBrainsMono-ExtraBold.woff2
│ │ ├── JetBrainsMono-ExtraLight.woff2
│ │ ├── JetBrainsMono-ThinItalic.woff2
│ │ ├── JetBrainsMono-LightItalic.woff2
│ │ ├── JetBrainsMono-MediumItalic.woff2
│ │ ├── JetBrainsMono-ExtraBoldItalic.woff2
│ │ ├── JetBrainsMono-SemiBoldItalic.woff2
│ │ └── JetBrainsMono-ExtraLightItalic.woff2
├── views
│ ├── errors
│ │ ├── 404.blade.php
│ │ ├── 401.blade.php
│ │ ├── 419.blade.php
│ │ ├── 500.blade.php
│ │ ├── 429.blade.php
│ │ ├── 503.blade.php
│ │ ├── 403.blade.php
│ │ └── layout.blade.php
│ └── app.blade.php
├── js
│ ├── Jetstream
│ │ ├── SectionBorder.svelte
│ │ ├── SuccessMessage.svelte
│ │ ├── ValidationErrors.svelte
│ │ ├── InputError.svelte
│ │ ├── Label.svelte
│ │ ├── AuthenticationCard.svelte
│ │ ├── DangerButton.svelte
│ │ ├── Button.svelte
│ │ ├── SecondaryButton.svelte
│ │ ├── SectionTitle.svelte
│ │ ├── DialogModal.svelte
│ │ ├── ActionSection.svelte
│ │ ├── Modal.svelte
│ │ ├── ConfirmationModal.svelte
│ │ └── FormSection.svelte
│ ├── Components
│ │ ├── Container.svelte
│ │ ├── FormLayouts
│ │ │ ├── Modals
│ │ │ │ ├── Container.svelte
│ │ │ │ └── Input.svelte
│ │ │ └── Inputs
│ │ │ │ ├── RadioGroup.svelte
│ │ │ │ ├── Checkbox.svelte
│ │ │ │ ├── Text.svelte
│ │ │ │ ├── Radio.svelte
│ │ │ │ └── FileUpload.svelte
│ │ ├── Widgets
│ │ │ ├── WidgetContainer.svelte
│ │ │ └── Widget.svelte
│ │ ├── Dropdowns
│ │ │ ├── InnerDropdownSection.svelte
│ │ │ ├── DropdownItem.svelte
│ │ │ └── Dropdown.svelte
│ │ ├── Checkbox.svelte
│ │ ├── Auth
│ │ │ ├── SubmitButton.svelte
│ │ │ └── AuthenticationCard.svelte
│ │ ├── Input.svelte
│ │ ├── Navigation
│ │ │ ├── MenuButton.svelte
│ │ │ └── MenuItem.svelte
│ │ ├── EmptyStates
│ │ │ ├── EmptyState.svelte
│ │ │ └── EmptyStateWithAction.svelte
│ │ ├── BreadcrumbNavigation
│ │ │ ├── BreadcrumbNavItem.svelte
│ │ │ └── BreadcrumbNavContainer.svelte
│ │ ├── Icons
│ │ │ └── Logo.svelte
│ │ ├── Toggles
│ │ │ └── Toggle.svelte
│ │ ├── Badge.svelte
│ │ └── Buttons
│ │ │ └── Button.svelte
│ ├── utils
│ │ ├── tag.js
│ │ ├── sidebar.js
│ │ ├── theme.js
│ │ ├── index.js
│ │ └── local-settings.js
│ ├── Heroicons
│ │ ├── Mini
│ │ │ ├── Minus.svelte
│ │ │ ├── Plus.svelte
│ │ │ ├── Moon.svelte
│ │ │ ├── PlusCircle.svelte
│ │ │ ├── Home.svelte
│ │ │ ├── Tag.svelte
│ │ │ ├── ComputerDesktop.svelte
│ │ │ ├── Link.svelte
│ │ │ ├── Inbox.svelte
│ │ │ └── Sun.svelte
│ │ ├── Micro
│ │ │ ├── Plus.svelte
│ │ │ └── XMark.svelte
│ │ └── Outline
│ │ │ ├── Link.svelte
│ │ │ └── Tag.svelte
│ ├── Layouts
│ │ ├── GuestLayout.svelte
│ │ └── AppLayout
│ │ │ └── Partials
│ │ │ └── Main.svelte
│ ├── app.js
│ ├── Pages
│ │ ├── Links
│ │ │ └── Index.svelte
│ │ ├── API
│ │ │ └── Index.svelte
│ │ ├── Tags
│ │ │ └── Index.svelte
│ │ ├── Profile
│ │ │ └── Partials
│ │ │ │ └── ExportUserDataForm.svelte
│ │ ├── Auth
│ │ │ ├── ConfirmPassword.svelte
│ │ │ ├── Login.svelte
│ │ │ └── VerifyEmail.svelte
│ │ └── Inbox
│ │ │ └── Index.svelte
│ ├── stores.js
│ ├── bootstrap.js
│ └── Partials
│ │ ├── DeleteLinkModal.svelte
│ │ ├── DeleteGroupModal.svelte
│ │ ├── DeletePublicLinkModal.svelte
│ │ └── DeleteTagModel.svelte
└── css
│ ├── fonts
│ └── inter.css
│ └── app.css
├── .gitattributes
├── jsconfig.json
├── docker
├── config
│ ├── custom-php-fpm.conf
│ └── custom-php.ini
├── .env.prod.example
├── compose.prod.yaml
└── mariadb-example
│ ├── .env.prod.example
│ └── compose.prod.yaml
├── Caddyfile
├── .styleci.yml
├── app
├── Http
│ ├── Controllers
│ │ ├── CsrfController.php
│ │ ├── Controller.php
│ │ ├── DeleteUserDataController.php
│ │ ├── BulkEditingController.php
│ │ ├── ApiControllers
│ │ │ ├── TagController.php
│ │ │ ├── GroupController.php
│ │ │ └── LinkController.php
│ │ ├── SearchController.php
│ │ ├── ImportController.php
│ │ ├── InboxController.php
│ │ └── ExportController.php
│ ├── Middleware
│ │ ├── PreventRequestsDuringMaintenance.php
│ │ ├── TrustHosts.php
│ │ ├── TrimStrings.php
│ │ ├── Authenticate.php
│ │ ├── AccessOnlyLocal.php
│ │ ├── TrustProxies.php
│ │ ├── RedirectIfAuthenticated.php
│ │ └── HandleInertiaRequests.php
│ └── Requests
│ │ ├── StoreGroupRequest.php
│ │ ├── UpdatePublicLinkRequest.php
│ │ ├── ExportRequest.php
│ │ ├── StorePublicLinkRequest.php
│ │ ├── DeleteUserDataRequest.php
│ │ ├── UpdateGroupRequest.php
│ │ ├── StoreLinkRequest.php
│ │ ├── ImportRequest.php
│ │ └── UpdateLinkRequest.php
├── Actions
│ ├── Fortify
│ │ ├── PasswordValidationRules.php
│ │ ├── ResetUserPassword.php
│ │ ├── CreateNewUser.php
│ │ ├── UpdateUserPassword.php
│ │ └── UpdateUserProfileInformation.php
│ └── Jetstream
│ │ └── DeleteUser.php
├── Providers
│ ├── BroadcastServiceProvider.php
│ ├── AuthServiceProvider.php
│ ├── EventServiceProvider.php
│ ├── JetstreamServiceProvider.php
│ ├── FortifyServiceProvider.php
│ └── RouteServiceProvider.php
├── Models
│ ├── PublicLink.php
│ ├── Tag.php
│ └── User.php
├── Helpers
│ ├── DateHelper.php
│ ├── WebpageData.php
│ └── PermissionHelper.php
├── Console
│ ├── Kernel.php
│ └── Commands
│ │ ├── UpdateGroupsLinkCount.php
│ │ ├── ShowStatsCommand.php
│ │ ├── ListUsersCommand.php
│ │ ├── CreateUserCommand.php
│ │ ├── DeleteUserCommand.php
│ │ └── DeleteDuplicateLinksCommand.php
├── Exceptions
│ └── Handler.php
└── Services
│ ├── DeleteUserDataService.php
│ ├── LinkService.php
│ ├── ExportService.php
│ └── BulkEditingService.php
├── .dockerignore
├── .gitignore
├── .editorconfig
├── docker-entrypoint.sh
├── vite.config.js
├── lang
├── en
│ ├── pagination.php
│ ├── auth.php
│ └── passwords.php
└── en.json
├── routes
├── channels.php
├── console.php
└── api.php
├── package.json
├── config
├── cors.php
├── services.php
├── view.php
├── hashing.php
└── broadcasting.php
├── phpunit.xml
├── .env.example
├── Dockerfile
└── artisan
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite*
2 | *.db*
3 |
--------------------------------------------------------------------------------
/bootstrap/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/storage/app/public/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/app/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !public/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/storage/framework/testing/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/views/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/cache/data/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/sessions/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !data/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/docs/images/home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/docs/images/home.jpg
--------------------------------------------------------------------------------
/public/assets/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/public/assets/favicons/favicon.ico
--------------------------------------------------------------------------------
/tests/Unit/ExampleTest.php:
--------------------------------------------------------------------------------
1 | toBeTrue();
5 | });
6 |
--------------------------------------------------------------------------------
/resources/markdown/policy.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | Edit this file to define the privacy policy for your application.
4 |
--------------------------------------------------------------------------------
/resources/markdown/terms.md:
--------------------------------------------------------------------------------
1 | # Terms of Service
2 |
3 | Edit this file to define the terms of service for your application.
4 |
--------------------------------------------------------------------------------
/public/assets/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/public/assets/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/assets/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/public/assets/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/resources/fonts/inter/InterVariable.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/inter/InterVariable.woff2
--------------------------------------------------------------------------------
/public/assets/favicons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/public/assets/favicons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/assets/favicons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/public/assets/favicons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/assets/favicons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/public/assets/favicons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/resources/fonts/inter/InterVariable-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/inter/InterVariable-Italic.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-Bold.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-Light.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-Thin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-Thin.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-Italic.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2
--------------------------------------------------------------------------------
/tests/Feature/ExampleTest.php:
--------------------------------------------------------------------------------
1 | get('/');
5 |
6 | $response->assertStatus(200);
7 | });
8 |
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-BoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-BoldItalic.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-ExtraBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-ExtraBold.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-ExtraLight.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-ExtraLight.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-ThinItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-ThinItalic.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-LightItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-LightItalic.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-MediumItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-MediumItalic.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-ExtraBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-ExtraBoldItalic.woff2
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-SemiBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-SemiBoldItalic.woff2
--------------------------------------------------------------------------------
/resources/views/errors/404.blade.php:
--------------------------------------------------------------------------------
1 | @extends('errors.minimal')
2 |
3 | @section('title', __('Not Found'))
4 | @section('code', '404')
5 | @section('message', __('Not Found'))
6 |
--------------------------------------------------------------------------------
/resources/fonts/jetbrains-mono/JetBrainsMono-ExtraLightItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beromir/Servas/HEAD/resources/fonts/jetbrains-mono/JetBrainsMono-ExtraLightItalic.woff2
--------------------------------------------------------------------------------
/resources/views/errors/401.blade.php:
--------------------------------------------------------------------------------
1 | @extends('errors::minimal')
2 |
3 | @section('title', __('Unauthorized'))
4 | @section('code', '401')
5 | @section('message', __('Unauthorized'))
6 |
--------------------------------------------------------------------------------
/resources/views/errors/419.blade.php:
--------------------------------------------------------------------------------
1 | @extends('errors::minimal')
2 |
3 | @section('title', __('Page Expired'))
4 | @section('code', '419')
5 | @section('message', __('Page Expired'))
6 |
--------------------------------------------------------------------------------
/resources/views/errors/500.blade.php:
--------------------------------------------------------------------------------
1 | @extends('errors::minimal')
2 |
3 | @section('title', __('Server Error'))
4 | @section('code', '500')
5 | @section('message', __('Server Error'))
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/resources/views/errors/429.blade.php:
--------------------------------------------------------------------------------
1 | @extends('errors::minimal')
2 |
3 | @section('title', __('Too Many Requests'))
4 | @section('code', '429')
5 | @section('message', __('Too Many Requests'))
6 |
--------------------------------------------------------------------------------
/resources/views/errors/503.blade.php:
--------------------------------------------------------------------------------
1 | @extends('errors::minimal')
2 |
3 | @section('title', __('Service Unavailable'))
4 | @section('code', '503')
5 | @section('message', __('Service Unavailable'))
6 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/SectionBorder.svelte:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/resources/views/errors/403.blade.php:
--------------------------------------------------------------------------------
1 | @extends('errors::minimal')
2 |
3 | @section('title', __('Forbidden'))
4 | @section('code', '403')
5 | @section('message', __($exception->getMessage() ?: 'Forbidden'))
6 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
3 | *.blade.php diff=html
4 | *.css diff=css
5 | *.html diff=html
6 | *.md diff=markdown
7 | *.php diff=php
8 |
9 | /.github export-ignore
10 | CHANGELOG.md export-ignore
11 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "@/*": [
6 | "resources/js/*"
7 | ]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/docker/config/custom-php-fpm.conf:
--------------------------------------------------------------------------------
1 | [www]
2 | user = root
3 | group = root
4 |
5 | pm = ondemand
6 | pm.max_children = 5
7 | pm.process_idle_timeout = 10s
8 | pm.max_requests = 200
9 | request_terminate_timeout = 60s
10 |
--------------------------------------------------------------------------------
/resources/js/Components/Container.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | {@render children?.()}
7 |
8 |
--------------------------------------------------------------------------------
/resources/js/utils/tag.js:
--------------------------------------------------------------------------------
1 | export function getTagIdsFromArray(tags) {
2 | let tagIds = [];
3 |
4 | tags.forEach((tag) => {
5 | tagIds = [...tagIds, tag.id];
6 | });
7 |
8 | return tagIds;
9 | }
10 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 |
2 | let { children, ...rest } = $props();
3 |
4 |
5 |
6 | {@render children?.()}
7 |
8 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/SuccessMessage.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | {#if show}
6 |
7 | {message}
8 |
9 | {/if}
10 |
--------------------------------------------------------------------------------
/resources/js/utils/sidebar.js:
--------------------------------------------------------------------------------
1 | import {dispatchCustomEvent} from "@/utils/index.js";
2 |
3 | function closeSidebar() {
4 | if (window.innerWidth >= 1024) {
5 | return;
6 | }
7 |
8 | dispatchCustomEvent('toggleSidebar');
9 | }
10 |
11 | export {closeSidebar};
12 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | .editorconfig
3 | .env
4 | .env.example
5 | .gitattributes
6 | .gitignore
7 | .styleci.yml
8 | phpunit.xml
9 | README.md
10 | ray.php
11 | LICENSE
12 | Dockerfile
13 |
14 | /.git
15 | /.idea
16 | /node_modules
17 | /vendor
18 | /docker
19 | !/docker/config/*
20 | /docs
21 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/ValidationErrors.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | {#if hasErrors}
6 |
7 |
Whoops! Something went wrong.
8 |
9 | {/if}
10 |
--------------------------------------------------------------------------------
/docker/.env.prod.example:
--------------------------------------------------------------------------------
1 | APP_NAME=Servas
2 | APP_ENV=production
3 | APP_KEY=
4 | APP_DEBUG=false
5 | APP_URL=https://your-servas-instance
6 |
7 | SERVAS_ENABLE_REGISTRATION=true
8 | SERVAS_SHOW_APP_VERSION=true
9 |
10 | # SQLite
11 | DB_CONNECTION=sqlite
12 | DB_DATABASE=/app/database/database.sqlite
13 | DB_FOREIGN_KEYS=true
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /public/hot
3 | /public/storage
4 | /public/css
5 | /public/js
6 | /public/build
7 | /storage/*.key
8 | /vendor
9 | .env
10 | .env.backup
11 | .phpunit.cache
12 | docker-compose.override.yml
13 | Homestead.json
14 | Homestead.yaml
15 | npm-debug.log
16 | yarn-error.log
17 | /.idea
18 | /.vscode
19 | CLAUDE.md
20 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/InputError.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 | {#if !isUndefined(message)}
8 |
9 | {message}
10 |
11 | {/if}
12 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/Label.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 | {#if label}
8 |
9 | {label}
10 |
11 | {/if}
12 |
--------------------------------------------------------------------------------
/resources/js/Components/Widgets/WidgetContainer.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | {@render children?.()}
9 |
10 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/resources/js/Components/FormLayouts/Inputs/RadioGroup.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | {label}
7 |
8 | {@render children?.()}
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docker/compose.prod.yaml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | servas:
5 | image: beromir/servas
6 | container_name: servas
7 | restart: unless-stopped
8 | ports:
9 | - "8080:80"
10 | volumes:
11 | - ./.env:/app/.env
12 | - servas-db-sqlite:/app/database/database.sqlite
13 |
14 | volumes:
15 | servas-db-sqlite:
16 |
--------------------------------------------------------------------------------
/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ ! -f /app/database/database.sqlite ]; then
4 | touch /app/database/database.sqlite
5 | fi
6 |
7 | cd /app
8 |
9 | echo "Starting Migration..."
10 | php artisan migrate --force
11 |
12 | echo "Creating caches..."
13 | php artisan config:cache
14 | php artisan view:cache
15 |
16 | frankenphp run --config /etc/caddy/Caddyfile
17 |
--------------------------------------------------------------------------------
/resources/js/Components/Dropdowns/InnerDropdownSection.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | {#if title}
7 |
8 | {title}
9 |
10 | {/if}
11 | {@render children?.()}
12 |
13 |
--------------------------------------------------------------------------------
/docker/mariadb-example/.env.prod.example:
--------------------------------------------------------------------------------
1 | APP_NAME=Servas
2 | APP_ENV=production
3 | APP_KEY=
4 | APP_DEBUG=false
5 | APP_URL=https://your-servas-instance
6 |
7 | SERVAS_ENABLE_REGISTRATION=true
8 | SERVAS_SHOW_APP_VERSION=true
9 |
10 | # MySQL
11 | DB_CONNECTION=mysql
12 | DB_HOST=db
13 | DB_PORT=3306
14 | DB_DATABASE=servas_db
15 | DB_USERNAME=servas_db_user
16 | DB_PASSWORD=password
17 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Mini/Minus.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
8 |
9 |
--------------------------------------------------------------------------------
/resources/js/Components/Checkbox.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
7 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Micro/Plus.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
8 |
9 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Mini/Plus.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/Feature/BrowserSessionsTest.php:
--------------------------------------------------------------------------------
1 | actingAs($user = User::factory()->create());
7 |
8 | $response = $this->delete('/user/other-browser-sessions', [
9 | 'password' => 'password',
10 | ]);
11 |
12 | $response->assertSessionHasNoErrors();
13 | });
14 |
--------------------------------------------------------------------------------
/resources/js/Components/Auth/SubmitButton.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 | {title}
10 |
11 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/AuthenticationCard.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | {@render logo?.()}
7 |
8 |
9 | {@render children?.()}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Micro/XMark.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
8 |
9 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Controller.php:
--------------------------------------------------------------------------------
1 |
2 | import clsx from "clsx";
3 |
4 | let {
5 | type = 'text',
6 | value = $bindable(null),
7 | ...props
8 | } = $props();
9 |
10 |
11 |
13 |
--------------------------------------------------------------------------------
/resources/css/fonts/inter.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: InterVariable;
3 | font-style: normal;
4 | font-weight: 100 900;
5 | font-display: swap;
6 | src: url("../../fonts/inter/InterVariable.woff2") format("woff2");
7 | }
8 |
9 | @font-face {
10 | font-family: InterVariable;
11 | font-style: italic;
12 | font-weight: 100 900;
13 | font-display: swap;
14 | src: url("../../fonts/inter/InterVariable-Italic.woff2") format("woff2");
15 | }
16 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/DangerButton.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | clicked()} {...rest}
6 | class="inline-flex items-center justify-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-600 disabled:opacity-25 transition">
7 | {@render children?.()}
8 |
9 |
--------------------------------------------------------------------------------
/app/Http/Middleware/PreventRequestsDuringMaintenance.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [
15 | //
16 | ];
17 | }
18 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrustHosts.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | public function hosts()
15 | {
16 | return [
17 | $this->allSubdomainsOfApplicationUrl(),
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Mini/Moon.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrimStrings.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [
15 | 'current_password',
16 | 'password',
17 | 'password_confirmation',
18 | ];
19 | }
20 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Mini/PlusCircle.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/tests/Feature/ProfileInformationTest.php:
--------------------------------------------------------------------------------
1 | actingAs($user = User::factory()->create());
7 |
8 | $response = $this->put('/user/profile-information', [
9 | 'name' => 'Test Name',
10 | 'email' => 'test@example.com',
11 | ]);
12 |
13 | expect($user->fresh())
14 | ->name->toEqual('Test Name')
15 | ->email->toEqual('test@example.com');
16 | });
17 |
--------------------------------------------------------------------------------
/app/Actions/Jetstream/DeleteUser.php:
--------------------------------------------------------------------------------
1 | deleteProfilePhoto();
18 | $user->tokens->each->delete();
19 | $user->delete();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/Button.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 | {@render children?.()}
10 |
11 |
--------------------------------------------------------------------------------
/tests/CreatesApplication.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class)->bootstrap();
19 |
20 | return $app;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Providers/BroadcastServiceProvider.php:
--------------------------------------------------------------------------------
1 | morphTo();
16 | }
17 |
18 | public function getLink(): string
19 | {
20 | return url("share/$this->share_id");
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/database/factories/GroupFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class GroupFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition()
18 | {
19 | return [
20 | //
21 | ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vite';
2 | import laravel from 'laravel-vite-plugin';
3 | import {svelte} from '@sveltejs/vite-plugin-svelte';
4 | import tailwindcss from '@tailwindcss/vite';
5 |
6 | export default defineConfig({
7 | plugins: [
8 | laravel({
9 | input: [
10 | 'resources/js/app.js',
11 | ],
12 | refresh: true,
13 | }),
14 | svelte({
15 | /* plugin options */
16 | }),
17 | tailwindcss(),
18 | ],
19 | });
20 |
--------------------------------------------------------------------------------
/app/Models/Tag.php:
--------------------------------------------------------------------------------
1 | $this->name]);
15 |
16 | return new SearchResult(
17 | $this,
18 | $this->name,
19 | $url
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Mini/Home.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/SecondaryButton.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | clicked()}
6 | class="inline-flex items-center px-4 py-2 bg-white border border-gray-400 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-xs hover:bg-gray-50 active:text-gray-800 active:bg-gray-50 disabled:opacity-25 transition dark:bg-gray-800 dark:text-gray-100 dark:border-gray-600 dark:hover:bg-gray-700">
7 | {@render children?.()}
8 |
9 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Mini/Tag.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/resources/js/Components/Widgets/Widget.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
{title}
12 |
17 | {@render children?.()}
18 |
19 |
20 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Outline/Link.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
11 |
12 |
--------------------------------------------------------------------------------
/app/Http/Controllers/DeleteUserDataController.php:
--------------------------------------------------------------------------------
1 | validated('deleteOptions');
14 |
15 | $deleteUserDataService->deleteUserData($deleteOptions, Auth::user());
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/Http/Middleware/Authenticate.php:
--------------------------------------------------------------------------------
1 | expectsJson()) {
18 | return route('login');
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Servas",
3 | "short_name": "Servas",
4 | "icons": [
5 | {
6 | "src": "/assets/favicons/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/assets/favicons/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "start_url": "/",
17 | "theme_color": "#4e64b7",
18 | "background_color": "#e5e8eb",
19 | "display": "standalone"
20 | }
21 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/SectionTitle.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | {@render title?.()}
9 |
10 |
11 |
12 | {@render description?.()}
13 |
14 |
15 |
16 |
17 | {@render aside?.()}
18 |
19 |
20 |
--------------------------------------------------------------------------------
/resources/js/Components/Navigation/MenuButton.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 | {clicked(); closeSidebar()}} type="button"
9 | class={clsx('flex items-center gap-x-3 py-1.5 px-3 w-full text-white font-medium rounded-md hover:bg-white/10 dark:hover:bg-gray-700/60', rest.class)}>
10 |
11 | {@render children?.()}
12 |
13 |
14 | {title}
15 |
16 |
--------------------------------------------------------------------------------
/lang/en/pagination.php:
--------------------------------------------------------------------------------
1 | '« Previous',
17 | 'next' => 'Next »',
18 |
19 | ];
20 |
--------------------------------------------------------------------------------
/resources/js/Components/FormLayouts/Inputs/Checkbox.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 | {label}
16 |
17 |
18 | {#if error}
19 | {error}
20 | {/if}
21 |
--------------------------------------------------------------------------------
/app/Helpers/DateHelper.php:
--------------------------------------------------------------------------------
1 | format('d.m.Y');
17 | }
18 |
19 | public static function convertToDateTimeString(string $timestamp): string
20 | {
21 | return self::makeCarbonInstance($timestamp)->format('d.m.Y H:i:s');
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/resources/js/Layouts/GuestLayout.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
20 |
21 |
22 | {$title ? `${$title} | ${appName}` : appName}
23 |
24 |
25 | {@render children?.()}
26 |
--------------------------------------------------------------------------------
/routes/channels.php:
--------------------------------------------------------------------------------
1 | id === (int) $id;
18 | });
19 |
--------------------------------------------------------------------------------
/app/Http/Controllers/BulkEditingController.php:
--------------------------------------------------------------------------------
1 | handleLinkEditingAction($action, $links, $groups, $tags);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/database/migrations/2024_02_19_210136_increase_link_column_size.php:
--------------------------------------------------------------------------------
1 | string('link', 2048)->change();
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 |
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | create([
20 | 'name' => 'Admin',
21 | 'email' => 'admin@admin.com'
22 | ]);
23 |
24 | Link::factory(100)->create(['user_id' => 1]);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Controllers/ApiControllers/TagController.php:
--------------------------------------------------------------------------------
1 | filterByCurrentUser()
17 | ->get()
18 | ->transform(fn(Tag $tag) => [
19 | 'id' => $tag->id,
20 | 'name' => $tag->name,
21 | ])
22 | ->toArray();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/database/factories/LinkFactory.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class LinkFactory extends Factory
12 | {
13 | /**
14 | * Define the model's default state.
15 | *
16 | * @return array
17 | */
18 | public function definition()
19 | {
20 | return [
21 | 'link' => $this->faker->url(),
22 | 'title' => $this->faker->city(),
23 | 'user_id' => User::factory(),
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/resources/js/app.js:
--------------------------------------------------------------------------------
1 | import {mount} from "svelte";
2 | import {createInertiaApp} from '@inertiajs/svelte';
3 |
4 | import('./bootstrap');
5 | import '../css/app.css';
6 |
7 |
8 | const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';
9 |
10 | createInertiaApp({
11 | title: title => `${title} - ${appName}`,
12 | resolve: name => {
13 | const pages = import.meta.glob('./Pages/**/*.svelte', {eager: true});
14 | return pages[`./Pages/${name}.svelte`];
15 | },
16 | setup({el, App, props}) {
17 | mount(App, {target: el, props})
18 | },
19 | progress: {
20 | color: '#4e64b7',
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/routes/console.php:
--------------------------------------------------------------------------------
1 | comment(Inspiring::quote());
19 | })->purpose('Display an inspiring quote');
20 |
--------------------------------------------------------------------------------
/app/Http/Controllers/ApiControllers/GroupController.php:
--------------------------------------------------------------------------------
1 | filterByCurrentUser()
17 | ->get()
18 | ->transform(fn(Group $group) => [
19 | 'id' => $group->id,
20 | 'title' => $group->title,
21 | ])
22 | ->toArray();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Outline/Tag.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/lang/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.",
3 | "The :attribute must contain at least one number.": "The :attribute must contain at least one number.",
4 | "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.",
5 | "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.",
6 | "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute."
7 | }
8 |
--------------------------------------------------------------------------------
/resources/js/Components/Auth/AuthenticationCard.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
12 |
13 |
14 | {@render children?.()}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/resources/js/utils/theme.js:
--------------------------------------------------------------------------------
1 | import {getTheme, setTheme} from "@/utils/local-settings.js";
2 |
3 | export function initTheme() {
4 | const theme = getTheme();
5 |
6 | return changeTheme(theme);
7 | }
8 |
9 | export function changeTheme(theme = '') {
10 | if (theme !== '') {
11 | applyTheme(theme);
12 | setTheme(theme);
13 | return theme;
14 | }
15 |
16 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
17 | applyTheme(systemTheme);
18 | setTheme('');
19 | return '';
20 | }
21 |
22 | function applyTheme(theme) {
23 | document.documentElement.classList.toggle('dark', theme === 'dark');
24 | }
25 |
--------------------------------------------------------------------------------
/app/Http/Requests/StoreGroupRequest.php:
--------------------------------------------------------------------------------
1 | |string>
22 | */
23 | public function rules(): array
24 | {
25 | return Group::rules();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/resources/js/Components/EmptyStates/EmptyState.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
7 |
9 |
11 |
12 |
13 | {title}
14 |
15 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Mini/ComputerDesktop.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Mini/Link.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
10 |
12 |
13 |
--------------------------------------------------------------------------------
/resources/js/Pages/Links/Index.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/Http/Requests/UpdatePublicLinkRequest.php:
--------------------------------------------------------------------------------
1 | |string>
21 | */
22 | public function rules(): array
23 | {
24 | return [
25 | //
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/database/migrations/2022_11_26_194415_remove_media_type_column_from_links_table.php:
--------------------------------------------------------------------------------
1 | dropColumn('media_type');
18 | });
19 | }
20 |
21 | /**
22 | * Reverse the migrations.
23 | *
24 | * @return void
25 | */
26 | public function down()
27 | {
28 |
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/app/Http/Requests/ExportRequest.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | public function rules(): array
23 | {
24 | return [
25 | 'exportFormat' => 'required|in:json,html',
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/resources/js/Pages/API/Index.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
15 |
16 |
17 |
22 |
23 |
--------------------------------------------------------------------------------
/docker/mariadb-example/compose.prod.yaml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | db:
5 | image: mariadb:10.7.3
6 | restart: unless-stopped
7 | command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin
8 | environment:
9 | - MARIADB_ROOT_PASSWORD=${DB_PASSWORD}
10 | - MARIADB_USER=${DB_USERNAME}
11 | - MARIADB_PASSWORD=${DB_PASSWORD}
12 | - MARIADB_DATABASE=${DB_DATABASE}
13 | volumes:
14 | - servas-db-data:/var/lib/mysql
15 |
16 | servas:
17 | image: beromir/servas
18 | container_name: servas
19 | restart: unless-stopped
20 | depends_on:
21 | - db
22 | ports:
23 | - "8080:80"
24 | volumes:
25 | - ./.env:/app/.env
26 |
27 | volumes:
28 | servas-db-data:
29 |
--------------------------------------------------------------------------------
/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/js/Heroicons/Mini/Inbox.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/Http/Middleware/AccessOnlyLocal.php:
--------------------------------------------------------------------------------
1 | |string>
21 | */
22 | public function rules(): array
23 | {
24 | return [
25 | 'groupId' => 'required|integer|exists:App\Models\Group,id',
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Providers/AuthServiceProvider.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | protected $policies = [
16 | // 'App\Models\Model' => 'App\Policies\ModelPolicy',
17 | ];
18 |
19 | /**
20 | * Register any authentication / authorization services.
21 | *
22 | * @return void
23 | */
24 | public function boot()
25 | {
26 | $this->registerPolicies();
27 |
28 | //
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/resources/js/Components/FormLayouts/Inputs/Text.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
17 | {label}
18 |
19 |
20 |
21 |
22 | {#if error}
23 |
{error}
24 | {/if}
25 |
26 |
27 |
--------------------------------------------------------------------------------
/resources/js/Components/EmptyStates/EmptyStateWithAction.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | clicked()} type="button"
7 | class="block w-full border-2 border-gray-400 border-dashed rounded-lg p-12 text-center hover:border-gray-500 dark:border-gray-500 dark:hover:border-gray-400">
8 | {#if icon}
9 |
10 | {@render icon?.()}
11 |
12 | {/if}
13 |
14 |
15 | {title}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/Http/Requests/DeleteUserDataRequest.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | public function rules(): array
23 | {
24 | return [
25 | 'deleteOptions' => 'required|array',
26 | 'deleteOptions.*' => 'string',
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/resources/js/Components/FormLayouts/Inputs/Radio.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | currentValue = event.target.checked ? value : currentValue}
14 | {id} {name} {value} checked={value === currentValue} {...rest}
15 | class="border-gray-400 ring-0! ring-offset-0! focus-visible:outline-2 focus-visible:outline-primary-500 checked:bg-primary-600 checked:border-primary-600 dark:bg-gray-950 dark:border-gray-500">
16 | {label}
17 |
18 |
--------------------------------------------------------------------------------
/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/Requests/UpdateGroupRequest.php:
--------------------------------------------------------------------------------
1 | route('group'), $this->user()->id);
17 | }
18 |
19 | /**
20 | * Get the validation rules that apply to the request.
21 | *
22 | * @return array|string>
23 | */
24 | public function rules(): array
25 | {
26 | return Group::rules();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Console/Kernel.php:
--------------------------------------------------------------------------------
1 | command('inspire')->hourly();
19 | }
20 |
21 | /**
22 | * Register the commands for the application.
23 | *
24 | * @return void
25 | */
26 | protected function commands()
27 | {
28 | $this->load(__DIR__.'/Commands');
29 |
30 | require base_path('routes/console.php');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "type": "module",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "check": "npx sv check --output human"
8 | },
9 | "devDependencies": {
10 | "@sveltejs/vite-plugin-svelte": "^6.2.1",
11 | "@tailwindcss/forms": "^0.5.10",
12 | "@tailwindcss/typography": "^0.5.19",
13 | "@tailwindcss/vite": "^4.1.16",
14 | "axios": "^1.3",
15 | "clsx": "^2.0.0",
16 | "laravel-vite-plugin": "^2.0.1",
17 | "lodash": "^4.17.21",
18 | "sv": "^0.9.13",
19 | "svelte": "^5.0.0",
20 | "svelte-check": "^4.3.3",
21 | "tailwindcss": "^4.1.16",
22 | "vite": "^7.1.12"
23 | },
24 | "dependencies": {
25 | "@inertiajs/svelte": "^2.2.7"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/Feature/DeleteApiTokenTest.php:
--------------------------------------------------------------------------------
1 | actingAs($user = User::factory()->withPersonalTeam()->create());
10 | } else {
11 | $this->actingAs($user = User::factory()->create());
12 | }
13 |
14 | $token = $user->tokens()->create([
15 | 'name' => 'Test Token',
16 | 'token' => Str::random(40),
17 | 'abilities' => ['create', 'read'],
18 | ]);
19 |
20 | $response = $this->delete('/user/api-tokens/'.$token->id);
21 |
22 | expect($user->fresh()->tokens)->toHaveCount(0);
23 | })->skip(function () {
24 | return ! Features::hasApiFeatures();
25 | }, 'API support is not enabled.');
26 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Console/Commands/UpdateGroupsLinkCount.php:
--------------------------------------------------------------------------------
1 | updateLinksCount();
31 |
32 | $group->save();
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/DialogModal.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 | {@render title?.()}
19 |
20 |
21 |
22 | {@render content?.()}
23 |
24 |
25 |
26 |
27 | {@render footer?.()}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/Actions/Fortify/ResetUserPassword.php:
--------------------------------------------------------------------------------
1 | $this->passwordRules(),
24 | ])->validate();
25 |
26 | $user->forceFill([
27 | 'password' => Hash::make($input['password']),
28 | ])->save();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/database/migrations/2023_03_19_203332_add_expires_at_column_to_personal_access_tokens_table.php:
--------------------------------------------------------------------------------
1 | timestamp('expires_at')->nullable()->after('last_used_at');
15 | });
16 | }
17 |
18 | /**
19 | * Reverse the migrations.
20 | */
21 | public function down(): void
22 | {
23 | Schema::table('personal_access_tokens', function (Blueprint $table) {
24 | $table->dropColumn('expires_at');
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/resources/js/Components/FormLayouts/Inputs/FileUpload.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 | {label}
13 | input({ file: e.target.files[0] })} bind:this={fileInput}
14 | {...rest}
15 | class="block mt-1 w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-gray-100 file:text-gray-900 hover:file:bg-gray-200 hover:file:cursor-pointer dark:text-gray-400 dark:file:bg-gray-800 dark:file:text-gray-100 dark:file:border-gray-600 dark:hover:file:bg-gray-700"/>
16 |
17 |
--------------------------------------------------------------------------------
/docker/config/custom-php.ini:
--------------------------------------------------------------------------------
1 | ; PHP Production Settings - Security and Performance Optimized
2 |
3 | ; General Settings
4 | memory_limit = 128M
5 | max_execution_time = 30
6 | max_input_time = 30
7 | post_max_size = 8M
8 | upload_max_filesize = 8M
9 |
10 | ; OpCache Settings - Optimized for Production
11 | opcache.enable = 1
12 | opcache.memory_consumption = 64
13 | opcache.interned_strings_buffer = 8
14 | opcache.max_accelerated_files = 5000
15 | opcache.validate_timestamps = 0
16 | opcache.file_cache_consistency_checks = 0
17 | opcache.revalidate_freq = 2
18 | opcache.preload_user = root
19 | opcache.file_cache_only = 0
20 | opcache.file_cache = /var/cache/php/opcache
21 |
22 | ; Realpath Cache Settings
23 | realpath_cache_size = 4096K
24 | realpath_cache_ttl = 3600
25 |
26 | ; Resource Limits
27 | max_file_uploads = 5
28 | max_input_vars = 2000
29 | max_input_nesting_level = 64
30 |
--------------------------------------------------------------------------------
/app/Providers/EventServiceProvider.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 |
--------------------------------------------------------------------------------
/database/migrations/2024_04_05_185606_add_query_options_columns_to_groups_table.php:
--------------------------------------------------------------------------------
1 | json('query_options')->nullable()->after('parent_group_id');
15 | $table->integer('links_count')->default(0)->after('query_options');
16 | });
17 | }
18 |
19 | /**
20 | * Reverse the migrations.
21 | */
22 | public function down(): void
23 | {
24 | Schema::table('groups', function (Blueprint $table) {
25 | $table->dropColumn('query_options', 'links_count');
26 | });
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/app/Helpers/WebpageData.php:
--------------------------------------------------------------------------------
1 | get($filteredUrl)->body();
18 |
19 | preg_match_all('@.*(.*).*@sU', $html, $matches);
20 |
21 | if (empty($matches[1])) return '';
22 |
23 | $title = trim(html_entity_decode($matches[1][0]));
24 | $title = strip_tags($title);
25 |
26 | return mb_substr($title, 0, 255);
27 | } catch (RequestException) {
28 |
29 | return '';
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/Feature/CreateApiTokenTest.php:
--------------------------------------------------------------------------------
1 | actingAs($user = User::factory()->withPersonalTeam()->create());
9 | } else {
10 | $this->actingAs($user = User::factory()->create());
11 | }
12 |
13 | $response = $this->post('/user/api-tokens', [
14 | 'name' => 'Test Token',
15 | 'permissions' => [
16 | 'read',
17 | 'update',
18 | ],
19 | ]);
20 |
21 | expect($user->fresh()->tokens)->toHaveCount(1);
22 | expect($user->fresh()->tokens->first())
23 | ->name->toEqual('Test Token')
24 | ->can('read')->toBeTrue()
25 | ->can('delete')->toBeFalse();
26 | })->skip(function () {
27 | return ! Features::hasApiFeatures();
28 | }, 'API support is not enabled.');
29 |
--------------------------------------------------------------------------------
/tests/Feature/AuthenticationTest.php:
--------------------------------------------------------------------------------
1 | get('/login');
8 |
9 | $response->assertStatus(200);
10 | });
11 |
12 | test('users can authenticate using the login screen', function () {
13 | $user = User::factory()->create();
14 |
15 | $response = $this->post('/login', [
16 | 'email' => $user->email,
17 | 'password' => 'password',
18 | ]);
19 |
20 | $this->assertAuthenticated();
21 | $response->assertRedirect(RouteServiceProvider::HOME);
22 | });
23 |
24 | test('users cannot authenticate with invalid password', function () {
25 | $user = User::factory()->create();
26 |
27 | $this->post('/login', [
28 | 'email' => $user->email,
29 | 'password' => 'wrong-password',
30 | ]);
31 |
32 | $this->assertGuest();
33 | });
34 |
--------------------------------------------------------------------------------
/resources/js/Heroicons/Mini/Sun.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
10 |
11 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/ActionSection.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 | {#snippet title()}
13 |
14 | {@render title_render?.()}
15 |
16 | {/snippet}
17 | {#snippet description()}
18 |
19 | {@render description_render?.()}
20 |
21 | {/snippet}
22 |
23 |
24 |
25 |
26 | {@render content?.()}
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/Feature/DeleteAccountTest.php:
--------------------------------------------------------------------------------
1 | actingAs($user = User::factory()->create());
8 |
9 | $response = $this->delete('/user', [
10 | 'password' => 'password',
11 | ]);
12 |
13 | expect($user->fresh())->toBeNull();
14 | })->skip(function () {
15 | return ! Features::hasAccountDeletionFeatures();
16 | }, 'Account deletion is not enabled.');
17 |
18 | test('correct password must be provided before account can be deleted', function () {
19 | $this->actingAs($user = User::factory()->create());
20 |
21 | $response = $this->delete('/user', [
22 | 'password' => 'wrong-password',
23 | ]);
24 |
25 | expect($user->fresh())->not->toBeNull();
26 | })->skip(function () {
27 | return ! Features::hasAccountDeletionFeatures();
28 | }, 'Account deletion is not enabled.');
29 |
--------------------------------------------------------------------------------
/resources/js/stores.js:
--------------------------------------------------------------------------------
1 | import {writable} from "svelte/store";
2 |
3 | function createRefreshGroupsStorage() {
4 | const getData = () => {
5 | return null;
6 | };
7 | const {subscribe, set, update} = writable(getData());
8 |
9 | return {
10 | subscribe,
11 | set,
12 | update: () => update(() => Date.now()),
13 | reset: () => set(getData()),
14 | };
15 | }
16 |
17 | function createSelectedTagsStorage() {
18 | const getData = () => {
19 | return {tags: [], action: ''};
20 | };
21 | const {subscribe, set, update} = writable(getData());
22 |
23 | return {
24 | subscribe,
25 | set,
26 | update,
27 | reset: () => set(getData()),
28 | };
29 | }
30 |
31 | export const refreshLinks = writable(true);
32 | export const refreshTags = writable(true);
33 | export const refreshGroups = createRefreshGroupsStorage();
34 | export const selectedTags = createSelectedTagsStorage();
35 |
--------------------------------------------------------------------------------
/database/migrations/2024_03_02_180621_create_public_links_tables.php:
--------------------------------------------------------------------------------
1 | id();
15 |
16 | $table->string('share_id')->unique();
17 | $table->foreignId('user_id')->constrained()->cascadeOnDelete();
18 | $table->morphs('public_linkable');
19 |
20 | $table->timestamps();
21 |
22 | $table->unique(['public_linkable_type', 'public_linkable_id']);
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | */
29 | public function down(): void
30 | {
31 | Schema::dropIfExists('public_links');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/resources/js/Components/FormLayouts/Modals/Input.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
18 |
20 | {label}
21 |
22 |
23 |
24 |
25 | {#if error}
26 |
{error}
27 | {/if}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/resources/views/app.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | @routes
17 |
18 | @vite('resources/js/app.js')
19 | @inertiaHead
20 |
21 |
22 | @inertia
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/Exceptions/Handler.php:
--------------------------------------------------------------------------------
1 | >
14 | */
15 | protected $dontReport = [
16 | //
17 | ];
18 |
19 | /**
20 | * A list of the inputs that are never flashed for validation exceptions.
21 | *
22 | * @var array
23 | */
24 | protected $dontFlash = [
25 | 'current_password',
26 | 'password',
27 | 'password_confirmation',
28 | ];
29 |
30 | /**
31 | * Register the exception handling callbacks for the application.
32 | *
33 | * @return void
34 | */
35 | public function register()
36 | {
37 | $this->reportable(function (Throwable $e) {
38 | //
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/Console/Commands/ShowStatsCommand.php:
--------------------------------------------------------------------------------
1 | table(['Model', 'Count'], [
34 | ['Links', Link::all()->count()],
35 | ['Groups', Group::all()->count()],
36 | ['Tags', Tag::all()->count()],
37 | ]);
38 |
39 | return Command::SUCCESS;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/resources/js/Components/BreadcrumbNavigation/BreadcrumbNavItem.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
11 |
12 |
13 | {#if link !== ''}
14 |
16 | {title}
17 |
18 |
19 | {:else}
20 |
22 | {title}
23 |
24 | {/if}
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/resources/js/Components/Icons/Logo.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
9 |
12 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/resources/js/utils/index.js:
--------------------------------------------------------------------------------
1 | export const route = window.route
2 |
3 | export function toggleValueInArray(arr, item) {
4 | return arr.includes(item) ? arr.filter(i => i !== item) : [...arr, item]
5 | }
6 |
7 | export function dispatchCustomEvent(event, data = null) {
8 | if (data !== null) {
9 | window.dispatchEvent(new CustomEvent(event, {detail: data}));
10 | } else {
11 | window.dispatchEvent(new CustomEvent(event));
12 | }
13 | }
14 |
15 | /** Dispatch event on click outside of node */
16 | export function clickOutside(node) {
17 |
18 | const handleClick = event => {
19 | if (node && !node.contains(event.target) && !event.defaultPrevented) {
20 | node.dispatchEvent(
21 | new CustomEvent('click_outside', node)
22 | )
23 | }
24 | }
25 |
26 | document.addEventListener('click', handleClick, true);
27 |
28 | return {
29 | destroy() {
30 | document.removeEventListener('click', handleClick, true);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Middleware/RedirectIfAuthenticated.php:
--------------------------------------------------------------------------------
1 | check()) {
26 | return redirect(RouteServiceProvider::HOME);
27 | }
28 | }
29 |
30 | return $next($request);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Services/DeleteUserDataService.php:
--------------------------------------------------------------------------------
1 | deleteLinks($user);
16 | }
17 |
18 | if (in_array('groups', $deleteOptions)) {
19 | $this->deleteGroups($user);
20 | }
21 |
22 | if (in_array('tags', $deleteOptions)) {
23 | $this->deleteTags($user);
24 | }
25 | }
26 |
27 | public function deleteLinks(User $user)
28 | {
29 | Link::where('user_id', $user->id)->delete();
30 | }
31 |
32 | public function deleteGroups(User $user)
33 | {
34 | Group::where('user_id', $user->id)->delete();
35 | }
36 |
37 | public function deleteTags(User $user)
38 | {
39 | Tag::where('user_id', $user->id)->delete();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->morphs('tokenable');
17 | $table->text('name');
18 | $table->string('token', 64)->unique();
19 | $table->text('abilities')->nullable();
20 | $table->timestamp('last_used_at')->nullable();
21 | $table->timestamp('expires_at')->nullable()->index();
22 | $table->timestamps();
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | */
29 | public function down(): void
30 | {
31 | Schema::dropIfExists('personal_access_tokens');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/resources/js/bootstrap.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | window._ = _;
4 |
5 | /**
6 | * We'll load the axios HTTP library which allows us to easily issue requests
7 | * to our Laravel back-end. This library automatically handles sending the
8 | * CSRF token as a header based on the value of the "XSRF" token cookie.
9 | */
10 |
11 | import axios from 'axios';
12 |
13 | window.axios = axios;
14 |
15 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
16 |
17 | /**
18 | * Echo exposes an expressive API for subscribing to channels and listening
19 | * for events that are broadcast by Laravel. Echo and event broadcasting
20 | * allows your team to easily build robust real-time web applications.
21 | */
22 |
23 | // import Echo from 'laravel-echo';
24 |
25 | // import Pusher from 'pusher-js';
26 | // window.Pusher = Pusher;
27 |
28 | // window.Echo = new Echo({
29 | // broadcaster: 'pusher',
30 | // key: import.meta.env.VITE_PUSHER_APP_KEY,
31 | // cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
32 | // forceTLS: true
33 | // });
34 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/routes/api.php:
--------------------------------------------------------------------------------
1 | group(function () {
21 | Route::get('/user', function (Request $request) {
22 | return $request->user();
23 | });
24 |
25 | Route::get('/all-tags', [TagController::class, 'getAllTags']);
26 | Route::get('/all-groups', [GroupController::class, 'getAllGroups']);
27 |
28 | Route::post('/links', [LinkController::class, 'store']);
29 | });
30 |
--------------------------------------------------------------------------------
/resources/js/Components/Dropdowns/DropdownItem.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 | clicked()}
19 | class={['flex items-center px-4 py-2 w-full group text-gray-700 text-sm hover:bg-gray-100 dark:text-gray-50 dark:hover:bg-gray-800', textColorClassNames].join(' ').trim()}
20 | role="menuitem" tabindex="-1" id="menu-item-0">
21 | {#if icon}
22 |
23 | {@render icon?.()}
24 |
25 | {/if}
26 | {title}
27 |
28 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/Modal.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 | {#if show}
22 |
23 |
26 |
27 |
29 | {@render children?.()}
30 |
31 |
32 | {/if}
33 |
--------------------------------------------------------------------------------
/tests/Feature/PasswordConfirmationTest.php:
--------------------------------------------------------------------------------
1 | withPersonalTeam()->create()
9 | : User::factory()->create();
10 |
11 | $response = $this->actingAs($user)->get('/user/confirm-password');
12 |
13 | $response->assertStatus(200);
14 | });
15 |
16 | test('password can be confirmed', function () {
17 | $user = User::factory()->create();
18 |
19 | $response = $this->actingAs($user)->post('/user/confirm-password', [
20 | 'password' => 'password',
21 | ]);
22 |
23 | $response->assertRedirect();
24 | $response->assertSessionHasNoErrors();
25 | });
26 |
27 | test('password is not confirmed with invalid password', function () {
28 | $user = User::factory()->create();
29 |
30 | $response = $this->actingAs($user)->post('/user/confirm-password', [
31 | 'password' => 'wrong-password',
32 | ]);
33 |
34 | $response->assertSessionHasErrors();
35 | });
36 |
--------------------------------------------------------------------------------
/tests/Feature/ApiTokenPermissionsTest.php:
--------------------------------------------------------------------------------
1 | actingAs($user = User::factory()->withPersonalTeam()->create());
10 | } else {
11 | $this->actingAs($user = User::factory()->create());
12 | }
13 |
14 | $token = $user->tokens()->create([
15 | 'name' => 'Test Token',
16 | 'token' => Str::random(40),
17 | 'abilities' => ['create', 'read'],
18 | ]);
19 |
20 | $response = $this->put('/user/api-tokens/'.$token->id, [
21 | 'name' => $token->name,
22 | 'permissions' => [
23 | 'delete',
24 | 'missing-permission',
25 | ],
26 | ]);
27 |
28 | expect($user->fresh()->tokens->first())
29 | ->can('delete')->toBeTrue()
30 | ->can('read')->toBeFalse()
31 | ->can('missing-permission')->toBeFalse();
32 | })->skip(function () {
33 | return ! Features::hasApiFeatures();
34 | }, 'API support is not enabled.');
35 |
--------------------------------------------------------------------------------
/resources/js/Components/Navigation/MenuItem.svelte:
--------------------------------------------------------------------------------
1 |
24 |
25 |
27 |
28 | {@render children?.()}
29 |
30 |
31 | {title}
32 |
33 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./tests/Unit
6 |
7 |
8 | ./tests/Feature
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ./app
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/Http/Controllers/SearchController.php:
--------------------------------------------------------------------------------
1 | input('searchString') ?? '';
18 |
19 | return (new Search())
20 | ->registerModel(Link::class, 'title', 'link')
21 | ->registerModel(Group::class, 'title')
22 | ->registerModel(Tag::class, 'name')
23 | ->search($searchString, Auth::user())
24 | ->transform(fn(SearchResult $searchResult) => (object)[
25 | 'title' => $searchResult->title,
26 | 'url' => $searchResult->url,
27 | 'link' => $searchResult->searchable->link,
28 | 'type' => $searchResult->type,
29 | 'hash' => md5($searchResult->url),
30 | ])
31 | ->toArray();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Console/Commands/ListUsersCommand.php:
--------------------------------------------------------------------------------
1 | id,
37 | $user->name,
38 | $user->email,
39 | $user->created_at,
40 | ];
41 | }
42 |
43 | $this->table(['ID', 'Name', 'Email', 'Created at'], $userInfo);
44 |
45 | return Command::SUCCESS;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/Http/Requests/StoreLinkRequest.php:
--------------------------------------------------------------------------------
1 | merge([
24 | 'link' => filter_var($this->link, FILTER_VALIDATE_URL) ? $this->link : "https://$this->link",
25 | ]);
26 | }
27 |
28 | /**
29 | * Get the validation rules that apply to the request.
30 | *
31 | * @return array
32 | */
33 | public function rules(): array
34 | {
35 | return array_merge(
36 | [
37 | 'groups' => 'array',
38 | 'tags' => 'array'
39 | ],
40 | Link::rules($this->request->get('link')),
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/Providers/JetstreamServiceProvider.php:
--------------------------------------------------------------------------------
1 | configurePermissions();
29 |
30 | Jetstream::deleteUsersUsing(DeleteUser::class);
31 | }
32 |
33 | /**
34 | * Configure the permissions that are available within the application.
35 | *
36 | * @return void
37 | */
38 | protected function configurePermissions()
39 | {
40 | Jetstream::defaultApiTokenPermissions(['create']);
41 |
42 | Jetstream::permissions([
43 | 'create',
44 | // 'read',
45 | // 'update',
46 | // 'delete',
47 | ]);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/Actions/Fortify/CreateNewUser.php:
--------------------------------------------------------------------------------
1 | ['required', 'string', 'max:255'],
25 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
26 | 'password' => $this->passwordRules(),
27 | 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['required', 'accepted'] : '',
28 | ])->validate();
29 |
30 | return User::create([
31 | 'name' => $input['name'],
32 | 'email' => $input['email'],
33 | 'password' => Hash::make($input['password']),
34 | ]);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/resources/js/Components/BreadcrumbNavigation/BreadcrumbNavContainer.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
25 |
26 |
27 | {#each items as item}
28 |
29 | {/each}
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/database/migrations/2022_08_11_143315_add_two_factor_confirmed_column_to_users_table.php:
--------------------------------------------------------------------------------
1 | timestamp('two_factor_confirmed_at')
19 | ->after('two_factor_recovery_codes')
20 | ->nullable();
21 | }
22 | });
23 | }
24 |
25 | /**
26 | * Reverse the migrations.
27 | *
28 | * @return void
29 | */
30 | public function down()
31 | {
32 | Schema::table('users', function (Blueprint $table) {
33 | if (Fortify::confirmsTwoFactorAuthentication()) {
34 | $table->dropColumn('two_factor_confirmed_at');
35 | }
36 | });
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/resources/js/Components/Toggles/Toggle.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
19 | {title}
20 |
23 |
24 |
25 |
26 | {title}
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/Actions/Fortify/UpdateUserPassword.php:
--------------------------------------------------------------------------------
1 | ['required', 'string'],
24 | 'password' => $this->passwordRules(),
25 | ])->after(function ($validator) use ($user, $input) {
26 | if (! isset($input['current_password']) || ! Hash::check($input['current_password'], $user->password)) {
27 | $validator->errors()->add('current_password', __('The provided password does not match your current password.'));
28 | }
29 | })->validateWithBag('updatePassword');
30 |
31 | $user->forceFill([
32 | 'password' => Hash::make($input['password']),
33 | ])->save();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/database/migrations/2023_04_30_173655_drop_later_table.php:
--------------------------------------------------------------------------------
1 | get();
16 |
17 | // Move all links from the later list to a new group
18 | foreach ($laterLinks as $laterLink) {
19 | $link = Link::find($laterLink->link_id);
20 |
21 | if (!($group = Group::where('title', 'Later list from migration')->where('user_id', $link->user_id)->first())) {
22 | $group = Group::make();
23 |
24 | $group->title = 'Later list from migration';
25 | $group->user_id = $link->user_id;
26 |
27 | $group->save();
28 | }
29 |
30 | if (!$group->links()->find($link->id)) {
31 | $group->links()->attach($link->id);
32 | }
33 | }
34 |
35 | Schema::table('later', function (Blueprint $table) {
36 | $table->dropIfExists();
37 | });
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/app/Http/Requests/ImportRequest.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | public function rules(): array
24 | {
25 | $rules = [
26 | 'importSource' => 'required|in:json,html',
27 | 'importOptions' => 'required|array',
28 | 'importOptions.*' => 'string',
29 | 'importFile' => [
30 | 'required',
31 | ],
32 | ];
33 |
34 | switch ($this->importSource) {
35 | case 'json':
36 | $rules['importFile'][] = File::types('json');
37 | break;
38 | case 'html':
39 | $rules['importFile'][] = File::types('html');
40 | break;
41 | }
42 |
43 | return $rules;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/Http/Controllers/ImportController.php:
--------------------------------------------------------------------------------
1 | validated();
17 |
18 | $importSource = $validated['importSource'];
19 | $importOptions = $validated['importOptions'];
20 |
21 | /** @var UploadedFile $importFile */
22 | $importFile = $validated['importFile'];
23 |
24 | if ($importSource === 'json' && Str::isJson($importFile->getContent())) {
25 | $data = json_decode($importFile->getContent(), true);
26 | } elseif ($importSource === 'html') {
27 | $data = $htmlBookmarkImportService->extractData($importFile->getContent());
28 | } else {
29 |
30 | return back();
31 | }
32 |
33 | $importService->importUserData($data, $importOptions, Auth::user());
34 |
35 | return back();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/Helpers/PermissionHelper.php:
--------------------------------------------------------------------------------
1 | $userTable === $userId;
19 | }
20 |
21 | /**
22 | * Check if the user is the owner of the modal instance with the ID specified in the array.
23 | * Return an array with the IDs that passed the check.
24 | */
25 | public static function filterArrayOfModalIds(array $modalIds, string $modal): array
26 | {
27 | if (empty($modalIds)) {
28 | return [];
29 | }
30 |
31 | $filteredModalIds = [];
32 | $modalInstance = app($modal);
33 |
34 | foreach ($modalIds as $modalId) {
35 | $modal = $modalInstance->find($modalId);
36 |
37 | if (self::userIsOwnerOfModal($modal)) {
38 | $filteredModalIds[] = $modalId;
39 | }
40 | }
41 |
42 | return $filteredModalIds;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/Http/Requests/UpdateLinkRequest.php:
--------------------------------------------------------------------------------
1 | route('link'), $this->user()->id);
17 | }
18 |
19 | /**
20 | * Prepare the data for validation.
21 | */
22 | protected function prepareForValidation(): void
23 | {
24 | $this->merge([
25 | 'link' => filter_var($this->link, FILTER_VALIDATE_URL) ? $this->link : "https://$this->link",
26 | ]);
27 | }
28 |
29 | /**
30 | * Get the validation rules that apply to the request.
31 | *
32 | * @return array
33 | */
34 | public function rules(): array
35 | {
36 | return array_merge(
37 | [
38 | 'groups' => 'array',
39 | 'tags' => 'array'
40 | ],
41 | Link::rules($this->request->get('link'), $this->route('link')->link),
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | APP_NAME=Servas
2 | APP_ENV=local
3 | APP_KEY=
4 | APP_DEBUG=true
5 | APP_URL=http://localhost
6 |
7 | SERVAS_ENABLE_REGISTRATION=true
8 | SERVAS_SHOW_APP_VERSION=true
9 |
10 | LOG_CHANNEL=stack
11 | LOG_DEPRECATIONS_CHANNEL=null
12 | LOG_LEVEL=debug
13 |
14 | # MySQL
15 | DB_CONNECTION=mysql
16 | DB_HOST=127.0.0.1
17 | DB_PORT=3306
18 | DB_DATABASE=laravel
19 | DB_USERNAME=root
20 | DB_PASSWORD=
21 |
22 | # SQLite
23 | #DB_CONNECTION=sqlite
24 | #DB_DATABASE=/app/database/database.sqlite
25 | #DB_FOREIGN_KEYS=true
26 |
27 | BROADCAST_DRIVER=log
28 | CACHE_DRIVER=file
29 | FILESYSTEM_DISK=local
30 | QUEUE_CONNECTION=sync
31 | SESSION_DRIVER=database
32 | SESSION_LIFETIME=120
33 |
34 | MEMCACHED_HOST=127.0.0.1
35 |
36 | REDIS_HOST=127.0.0.1
37 | REDIS_PASSWORD=null
38 | REDIS_PORT=6379
39 |
40 | MAIL_MAILER=smtp
41 | MAIL_HOST=mailhog
42 | MAIL_PORT=1025
43 | MAIL_USERNAME=null
44 | MAIL_PASSWORD=null
45 | MAIL_ENCRYPTION=null
46 | MAIL_FROM_ADDRESS=null
47 | MAIL_FROM_NAME="${APP_NAME}"
48 |
49 | AWS_ACCESS_KEY_ID=
50 | AWS_SECRET_ACCESS_KEY=
51 | AWS_DEFAULT_REGION=us-east-1
52 | AWS_BUCKET=
53 | AWS_USE_PATH_STYLE_ENDPOINT=false
54 |
55 | PUSHER_APP_ID=
56 | PUSHER_APP_KEY=
57 | PUSHER_APP_SECRET=
58 | PUSHER_APP_CLUSTER=mt1
59 |
60 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
61 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
62 |
--------------------------------------------------------------------------------
/tests/Feature/RegistrationTest.php:
--------------------------------------------------------------------------------
1 | get('/register');
9 |
10 | $response->assertStatus(200);
11 | })->skip(function () {
12 | return ! Features::enabled(Features::registration());
13 | }, 'Registration support is not enabled.');
14 |
15 | test('registration screen cannot be rendered if support is disabled', function () {
16 | $response = $this->get('/register');
17 |
18 | $response->assertStatus(404);
19 | })->skip(function () {
20 | return Features::enabled(Features::registration());
21 | }, 'Registration support is enabled.');
22 |
23 | test('new users can register', function () {
24 | $response = $this->post('/register', [
25 | 'name' => 'Test User',
26 | 'email' => 'test@example.com',
27 | 'password' => 'password',
28 | 'password_confirmation' => 'password',
29 | 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature(),
30 | ]);
31 |
32 | $this->assertAuthenticated();
33 | $response->assertRedirect(RouteServiceProvider::HOME);
34 | })->skip(function () {
35 | return ! Features::enabled(Features::registration());
36 | }, 'Registration support is not enabled.');
37 |
--------------------------------------------------------------------------------
/app/Http/Middleware/HandleInertiaRequests.php:
--------------------------------------------------------------------------------
1 | config('app.name'),
41 | 'appVersion' => config('app.version'),
42 | 'showAppVersion' => config('app.show_app_version'),
43 | ]);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/Http/Controllers/InboxController.php:
--------------------------------------------------------------------------------
1 | Inertia::scroll(fn() => Link::orderBy('created_at', 'desc')
23 | ->filterByCurrentUser()
24 | ->when($showUntagged, fn($query) => $query->whereDoesntHave('tags'))
25 | ->when($showUngrouped, fn($query) => $query->whereDoesntHave('groups'))
26 | ->filterLinks($searchString)
27 | ->through(fn(Link $link) => [
28 | 'title' => $link->title,
29 | 'link' => $link->link,
30 | 'id' => $link->id,
31 | ])),
32 | 'searchString' => $searchString,
33 | 'untagged' => $showUntagged,
34 | 'ungrouped' => $showUngrouped,
35 | ]);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/Http/Controllers/ExportController.php:
--------------------------------------------------------------------------------
1 | validated();
15 |
16 | $exportFormat = $validated['exportFormat'];
17 |
18 | $export = $exportService->exportUserData(Auth::user());
19 |
20 | if ($exportFormat === 'json') {
21 |
22 | return response()->streamDownload(function () use ($export) {
23 | echo json_encode($export, JSON_PRETTY_PRINT);
24 | }, 'export.json', [
25 | 'Content-Type' => 'application/json',
26 | ]);
27 | } elseif ($exportFormat === 'html' && array_key_exists('links', $export)) {
28 |
29 | return response()->streamDownload(function () use ($htmlBookmarkExportService, $export) {
30 | echo $htmlBookmarkExportService->createHtmlExport($export);
31 | }, 'export.html', [
32 | 'Content-Type' => 'text/html',
33 | ]);
34 | }
35 |
36 | return back();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/resources/js/Components/Dropdowns/Dropdown.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
20 | {#if showDropdown}
21 | showDropdown = false} {...rest}
22 | in:scale={{ duration: 100, easing: cubicOut, start: 0.95 }}
23 | out:scale={{ duration: 75, easing: cubicIn, start: 0.95 }}
24 | class={clsx(rest.class, openingDirection === 'right' ? 'left-0 origin-top-left' : 'right-0 origin-top-right', 'absolute z-10 mt-6 w-44 divide-y divide-gray-200 rounded-md bg-white shadow-lg ring-contrast md:w-56 dark:bg-gray-900 dark:divide-gray-700')}
25 | role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
26 | {@render children?.()}
27 |
28 | {/if}
29 |
--------------------------------------------------------------------------------
/resources/js/Components/Badge.svelte:
--------------------------------------------------------------------------------
1 |
33 |
34 |
36 | {title}
37 |
38 |
--------------------------------------------------------------------------------
/app/Services/LinkService.php:
--------------------------------------------------------------------------------
1 | id)->get(['id', 'link'])->groupBy('link')->toArray();;
16 |
17 | return array_filter($groupedLinks, fn($group) => count($group) > 1);
18 | }
19 |
20 | /**
21 | * Remove duplicate links.
22 | */
23 | public function deleteDuplicates(array $links): void
24 | {
25 | // TODO: Find a cleaner solution.
26 | foreach ($links as $duplicates) {
27 |
28 | if (!is_array($duplicates)) {
29 | continue;
30 | }
31 |
32 | array_pop($duplicates);
33 |
34 | if (empty($duplicates)) {
35 | continue;
36 | }
37 |
38 | foreach ($duplicates as $duplicateLink) {
39 | $link = Link::find($duplicateLink['id']);
40 |
41 | $this->deleteLink($link);
42 | }
43 | }
44 | }
45 |
46 | /**
47 | * Delete the link if not null.
48 | */
49 | public function deleteLink(?Link $link): void
50 | {
51 | if (is_null($link)) {
52 | return;
53 | }
54 |
55 | $link->delete();
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/resources/js/Partials/DeleteLinkModal.svelte:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 | {#if link}
32 |
33 | Are you sure you want to delete this link?
34 |
35 |
36 | {link.title}
37 |
38 | {/if}
39 |
40 | {#snippet footer()}
41 | modal.close()} title="Cancel" color="white"
42 | class="hidden sm:block"/>
43 |
44 | {/snippet}
45 |
46 |
--------------------------------------------------------------------------------
/tests/Feature/UpdatePasswordTest.php:
--------------------------------------------------------------------------------
1 | actingAs($user = User::factory()->create());
8 |
9 | $response = $this->put('/user/password', [
10 | 'current_password' => 'password',
11 | 'password' => 'new-password',
12 | 'password_confirmation' => 'new-password',
13 | ]);
14 |
15 | expect(Hash::check('new-password', $user->fresh()->password))->toBeTrue();
16 | });
17 |
18 | test('current password must be correct', function () {
19 | $this->actingAs($user = User::factory()->create());
20 |
21 | $response = $this->put('/user/password', [
22 | 'current_password' => 'wrong-password',
23 | 'password' => 'new-password',
24 | 'password_confirmation' => 'new-password',
25 | ]);
26 |
27 | $response->assertSessionHasErrors();
28 |
29 | expect(Hash::check('password', $user->fresh()->password))->toBeTrue();
30 | });
31 |
32 | test('new passwords must match', function () {
33 | $this->actingAs($user = User::factory()->create());
34 |
35 | $response = $this->put('/user/password', [
36 | 'current_password' => 'password',
37 | 'password' => 'new-password',
38 | 'password_confirmation' => 'wrong-password',
39 | ]);
40 |
41 | $response->assertSessionHasErrors();
42 |
43 | expect(Hash::check('password', $user->fresh()->password))->toBeTrue();
44 | });
45 |
--------------------------------------------------------------------------------
/resources/css/app.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 | @import './fonts/inter.css' layer(base);
3 | @import './fonts/jetbrains-mono.css' layer(base);
4 |
5 | @theme {
6 | /* Primary (Indigo) */
7 | --color-primary-900: hsl(234deg 62% 26%);
8 | --color-primary-800: hsl(232deg 51% 36%);
9 | --color-primary-700: hsl(230deg 49% 41%);
10 | --color-primary-600: hsl(228deg 45% 45%);
11 | --color-primary-500: hsl(227deg 42% 51%);
12 | --color-primary-400: hsl(227deg 50% 59%);
13 | --color-primary-300: hsl(225deg 57% 67%);
14 | --color-primary-200: hsl(224deg 67% 76%);
15 | --color-primary-100: hsl(221deg 78% 86%);
16 | --color-primary-50: hsl(221deg 68% 93%);
17 |
18 | /* Fonts */
19 | --font-sans: 'InterVariable', sans-serif;
20 | --font-mono: 'JetBrains Mono', monospace;
21 | }
22 |
23 | @plugin '@tailwindcss/forms';
24 | @plugin '@tailwindcss/typography';
25 |
26 | @custom-variant dark (&:where(.dark, .dark *));
27 |
28 | @custom-variant hover (&:where(:hover, :focus-visible));
29 |
30 | @layer base {
31 | body:has(dialog[open]) {
32 | overflow: hidden;
33 | }
34 |
35 | dialog {
36 | margin: 0;
37 | width: 100%;
38 | max-width: 100%;
39 | }
40 |
41 | button:focus-visible,
42 | a:focus-visible {
43 | outline: 2px solid var(--color-primary-400);
44 | outline-offset: 2px;
45 | }
46 | }
47 |
48 | @utility ring-contrast {
49 | @apply ring-1 ring-black/5 dark:ring-white/15;
50 | }
51 |
--------------------------------------------------------------------------------
/resources/js/Partials/DeleteGroupModal.svelte:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 | {#if group}
32 |
33 | Are you sure you want to delete this group?
34 |
35 |
36 | {group.title}
37 |
38 | {/if}
39 |
40 | {#snippet footer()}
41 | modal.close()} title="Cancel" color="white"
42 | class="hidden sm:block"/>
43 |
44 | {/snippet}
45 |
46 |
--------------------------------------------------------------------------------
/resources/js/Pages/Tags/Index.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {#each tags as tag (tag.id)}
28 | newTagComponent.initEditMode(tag)} class="mr-2 first:mt-4 last:mr-0"
29 | title={tag.name}/>
30 | {:else}
31 | newTagComponent.focus()} title="Add a new tag"
32 | class="mt-2 sm:mt-4">
33 | {#snippet icon()}
34 |
35 | {/snippet}
36 |
37 | {/each}
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/Models/User.php:
--------------------------------------------------------------------------------
1 | 'datetime',
51 | ];
52 |
53 | /**
54 | * The accessors to append to the model's array form.
55 | *
56 | * @var array
57 | */
58 | protected $appends = [
59 | 'profile_photo_url',
60 | ];
61 | }
62 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM dunglas/frankenphp:php8.4-alpine AS application_builder
2 | WORKDIR /app
3 |
4 | COPY . ./
5 |
6 | RUN install-php-extensions \
7 | @composer
8 |
9 | RUN composer install --optimize-autoloader --no-dev && \
10 | composer dump-autoload --no-dev --classmap-authoritative
11 |
12 |
13 | FROM node:24.11-alpine AS asset_builder
14 | WORKDIR /app
15 |
16 | COPY ./package.json ./
17 | COPY ./package-lock.json ./
18 | COPY ./vite.config.js ./
19 | COPY ./resources ./resources
20 |
21 | RUN npm install && \
22 | npm run build
23 |
24 |
25 | FROM dunglas/frankenphp:php8.4-alpine
26 | WORKDIR /app
27 |
28 | ENV SERVER_NAME=:80
29 |
30 | RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
31 |
32 | RUN install-php-extensions \
33 | pdo_mysql \
34 | mysqli \
35 | opcache \
36 | bcmath \
37 | && apk add --no-cache \
38 | mariadb-client \
39 | sqlite
40 |
41 | COPY . ./
42 |
43 | COPY --from=application_builder /app/vendor ./vendor
44 | COPY --from=application_builder /app/bootstrap/cache ./bootstrap/cache
45 |
46 | COPY --from=asset_builder /app/public/build ./public/build
47 |
48 | RUN rm -rf ./docker
49 |
50 | COPY ./Caddyfile /etc/caddy/Caddyfile
51 | COPY docker/config/custom-php.ini /usr/local/etc/php/conf.d/zzz-custom-php.ini
52 | COPY docker/config/custom-php-fpm.conf /usr/local/etc/php-fpm.d/zzz-custom-php-fpm.conf
53 |
54 | RUN mkdir -p /var/cache/php/opcache && \
55 | chmod 700 /var/cache/php/opcache
56 |
57 | COPY ./docker-entrypoint.sh /
58 |
59 | ENTRYPOINT ["/docker-entrypoint.sh"]
60 |
--------------------------------------------------------------------------------
/resources/js/Partials/DeletePublicLinkModal.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 |
33 | Are you sure you want to delete this public link and stop the sharing of the group?
34 |
35 |
36 | {publicLinkTitle}
37 |
38 |
39 | {#snippet footer()}
40 | modal.close()} title="Cancel" color="white" class="hidden sm:block"/>
41 |
42 | {/snippet}
43 |
44 |
--------------------------------------------------------------------------------
/tests/Feature/TwoFactorAuthenticationSettingsTest.php:
--------------------------------------------------------------------------------
1 | actingAs($user = User::factory()->create());
7 |
8 | $this->withSession(['auth.password_confirmed_at' => time()]);
9 |
10 | $response = $this->post('/user/two-factor-authentication');
11 |
12 | expect($user->fresh()->two_factor_secret)->not->toBeNull();
13 | expect($user->fresh()->recoveryCodes())->toHaveCount(8);
14 | });
15 |
16 | test('recovery codes can be regenerated', function () {
17 | $this->actingAs($user = User::factory()->create());
18 |
19 | $this->withSession(['auth.password_confirmed_at' => time()]);
20 |
21 | $this->post('/user/two-factor-authentication');
22 | $this->post('/user/two-factor-recovery-codes');
23 |
24 | $user = $user->fresh();
25 |
26 | $this->post('/user/two-factor-recovery-codes');
27 |
28 | expect($user->recoveryCodes())->toHaveCount(8);
29 | expect(array_diff($user->recoveryCodes(), $user->fresh()->recoveryCodes()))->toHaveCount(8);
30 | });
31 |
32 | test('two factor authentication can be disabled', function () {
33 | $this->actingAs($user = User::factory()->create());
34 |
35 | $this->withSession(['auth.password_confirmed_at' => time()]);
36 |
37 | $this->post('/user/two-factor-authentication');
38 |
39 | $this->assertNotNull($user->fresh()->two_factor_secret);
40 |
41 | $this->delete('/user/two-factor-authentication');
42 |
43 | expect($user->fresh()->two_factor_secret)->toBeNull();
44 | });
45 |
--------------------------------------------------------------------------------
/app/Providers/FortifyServiceProvider.php:
--------------------------------------------------------------------------------
1 | email;
41 |
42 | return Limit::perMinute(5)->by($email.$request->ip());
43 | });
44 |
45 | RateLimiter::for('two-factor', function (Request $request) {
46 | return Limit::perMinute(5)->by($request->session()->get('login.id'));
47 | });
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/resources/js/Partials/DeleteTagModel.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
34 | Are you sure you want to delete this tag?
35 |
36 |
37 | {tag.name}
38 |
39 |
40 | {#snippet footer()}
41 |
43 | modal.close()} title="Cancel" color="white"
44 | class="hidden mt-3 sm:inline-flex sm:mt-0"/>
45 | {/snippet}
46 |
47 |
--------------------------------------------------------------------------------
/app/Console/Commands/CreateUserCommand.php:
--------------------------------------------------------------------------------
1 | ask('What is your name?');
34 | $email = $this->ask('What is your email?');
35 | $password = $this->secret('What is the password?');
36 | $confirmPassword = $this->secret('Confirm the password?');
37 |
38 | try {
39 | $createNewUser->create([
40 | 'name' => $name,
41 | 'email' => $email,
42 | 'password' => $password,
43 | 'password_confirmation' => $confirmPassword
44 | ]);
45 | } catch (ValidationException $exception) {
46 | $this->error($exception->getMessage());
47 | return Command::FAILURE;
48 | }
49 |
50 | $this->info('The user was created!');
51 |
52 | return Command::SUCCESS;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/resources/views/errors/layout.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | @yield('title')
8 |
9 |
10 |
43 |
44 |
45 |
46 |
47 |
48 | @yield('message')
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/ConfirmationModal.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
25 |
26 |
27 |
28 | {@render title?.()}
29 |
30 |
31 |
32 | {@render content?.()}
33 |
34 |
35 |
36 |
37 |
38 |
39 | {@render footer?.()}
40 |
41 |
42 |
--------------------------------------------------------------------------------
/resources/js/Pages/Profile/Partials/ExportUserDataForm.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 | {#snippet title()}
25 | Export Data
26 | {/snippet}
27 |
28 | {#snippet description()}
29 | Export your account data as JSON or HTML.
30 | {/snippet}
31 |
32 | {#snippet form()}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {/snippet}
42 |
43 | {#snippet actions()}
44 |
45 | Export
46 |
47 | {/snippet}
48 |
49 |
--------------------------------------------------------------------------------
/app/Console/Commands/DeleteUserCommand.php:
--------------------------------------------------------------------------------
1 | ask('User ID');
34 |
35 | try {
36 | Validator::make(['id' => $userId], [
37 | 'id' => ['required', 'numeric'],
38 | ])->validate();
39 | } catch (ValidationException $exception) {
40 | $this->error($exception->getMessage());
41 | return Command::FAILURE;
42 | }
43 |
44 | $user = User::find($userId);
45 |
46 | if (empty($user)) {
47 | $this->error('The user does not exist.');
48 |
49 | return Command::FAILURE;
50 | }
51 |
52 | if ($this->confirm("Are you sure you want to delete the user with the email: $user->email?")) {
53 | $user->delete();
54 |
55 | $this->info('The user was deleted!');
56 |
57 | return Command::SUCCESS;
58 | }
59 |
60 | $this->error('Please confirm the deletion.');
61 |
62 | return Command::FAILURE;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/resources/js/Components/Buttons/Button.svelte:
--------------------------------------------------------------------------------
1 |
34 |
35 | svg]:size-5',
41 | title ? 'px-4 [&>svg]:-ml-1 [&>svg]:mr-2' : 'px-2',
42 | getColors(),
43 | rest.class
44 | )}
45 | title={hoverTitle}>
46 | {#if icon}
47 | {@render icon?.()}
48 | {/if}
49 | {#if title}
50 | {title}
51 | {/if}
52 |
53 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/Pest.php:
--------------------------------------------------------------------------------
1 | in('Feature');
18 |
19 | /*
20 | |--------------------------------------------------------------------------
21 | | Expectations
22 | |--------------------------------------------------------------------------
23 | |
24 | | When you're writing tests, you often need to check that values meet certain conditions. The
25 | | "expect()" function gives you access to a set of "expectations" methods that you can use
26 | | to assert different things. Of course, you may extend the Expectation API at any time.
27 | |
28 | */
29 |
30 | expect()->extend('toBeOne', function () {
31 | return $this->toBe(1);
32 | });
33 |
34 | /*
35 | |--------------------------------------------------------------------------
36 | | Functions
37 | |--------------------------------------------------------------------------
38 | |
39 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your
40 | | project that you don't want to repeat in every file. Here you can also expose helpers as
41 | | global functions to help you to reduce the number of lines of code in your test files.
42 | |
43 | */
44 |
45 | function something()
46 | {
47 | // ..
48 | }
49 |
--------------------------------------------------------------------------------
/app/Http/Controllers/ApiControllers/LinkController.php:
--------------------------------------------------------------------------------
1 | validated();
29 |
30 | $link = Link::make();
31 |
32 | $link->link = $validated['link'];
33 | $link->title = $validated['title'];
34 | $link->user_id = Auth::id();
35 |
36 | if (empty($link->title)) {
37 | $link->title = WebpageData::getWebPageTitle($link->link);
38 | }
39 |
40 | $link->save();
41 |
42 | if (key_exists('groups', $validated)) {
43 | $groupIds = $validated['groups'];
44 |
45 | $link->groups()->sync($groupIds);
46 |
47 | $this->groupService->updateUserGroupsLinkCount(Auth::user());
48 | }
49 |
50 | if (key_exists('tags', $validated)) {
51 | $tags = [];
52 |
53 | foreach ($validated['tags'] as $tag) {
54 | $tags[] = Tag::filterByCurrentUser()->find($tag);
55 | }
56 |
57 | $link->syncTags($tags);
58 | }
59 |
60 | return response('The link was added.', headers: ['Content-Type' => 'text/plain']);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/Services/ExportService.php:
--------------------------------------------------------------------------------
1 | id)
17 | ->get()
18 | ->transform(fn(Link $link) => [
19 | 'title' => trim($link->title),
20 | 'link' => $link->link,
21 | 'createdAt' => $link->created_at->jsonSerialize(),
22 | 'updatedAt' => $link->updated_at->jsonSerialize(),
23 | 'groups' => $link
24 | ->groups()
25 | ->get(['title'])
26 | ->transform(fn(Group $group) => $group->title)
27 | ->toArray(),
28 | 'tags' => $link
29 | ->tags()
30 | ->get()
31 | ->transform(fn(Tag $tag) => $tag->name)
32 | ->toArray()
33 | ])
34 | ->toArray();
35 |
36 | $export['groups'] = Group::where('user_id', $user->id)
37 | ->get()
38 | ->transform(fn(Group $group) => [
39 | 'title' => $group->title,
40 | 'childGroups' => $group
41 | ->groups()
42 | ->get()
43 | ->transform(fn(Group $group) => $group->title)
44 | ->toArray(),
45 | ])
46 | ->toArray();
47 |
48 | $export['tags'] = Tag::where('user_id', $user->id)
49 | ->get()
50 | ->transform(fn(Tag $tag) => $tag->name)
51 | ->toArray();
52 |
53 | return $export;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Services/BulkEditingService.php:
--------------------------------------------------------------------------------
1 | delete($link);
32 | break;
33 | case 'attachGroups':
34 | $linkService->attachOrDetachGroups($link, $groups, true);
35 | break;
36 | case 'detachGroups':
37 | $linkService->attachOrDetachGroups($link, $groups, false);
38 | break;
39 | case 'attachTags':
40 | $linkService->attachOrDetachTags($link, $tags, true);
41 | break;
42 | case 'detachTags':
43 | $linkService->attachOrDetachTags($link, $tags, false);
44 | break;
45 | default:
46 | return;
47 | }
48 | }
49 |
50 | $groupService = app(GroupService::class);
51 |
52 | $groupService->updateUserGroupsLinkCount(Auth::user());
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/resources/js/Layouts/AppLayout/Partials/Main.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 | {#if title}
16 |
18 | {#if title}
19 |
{title}
20 | {/if}
21 | {#if toolbar}{@render toolbar()}{:else}
22 |
23 | {/if}
24 |
25 | {#if showNewLinkButton}
26 |
27 |
dispatchCustomEvent('prepareCreateNewLink')} title="new link">
28 | {#snippet icon()}
29 |
30 |
33 |
34 | {/snippet}
35 |
36 |
37 | {/if}
38 |
39 | {/if}
40 |
41 |
42 |
43 | {@render children?.()}
44 |
45 |
46 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/resources/js/Jetstream/FormSection.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 | {#snippet title()}
34 | {@render title_render?.()}
35 | {/snippet}
36 | {#snippet description()}
37 | {@render description_render?.()}
38 | {/snippet}
39 |
40 |
41 |
58 |
59 |
--------------------------------------------------------------------------------
/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($request->user()?->id ?: $request->ip());
61 | });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/resources/js/utils/local-settings.js:
--------------------------------------------------------------------------------
1 | function getLocalSettings() {
2 | const settings = localStorage.getItem('servas_settings');
3 |
4 | return settings !== null ? JSON.parse(settings) : {};
5 | }
6 |
7 | function setLocalSettings(name, settingsToSet) {
8 | let settings = getLocalSettings();
9 |
10 | switch (name) {
11 | case 'openedGroups':
12 | settings.openedGroups = settingsToSet;
13 | break;
14 |
15 | case 'sidebarIsOpen':
16 | settings.sidebarIsOpen = settingsToSet;
17 | break;
18 |
19 | case 'theme':
20 | settings.theme = settingsToSet;
21 | break;
22 | }
23 |
24 | localStorage.setItem('servas_settings', JSON.stringify(settings));
25 | }
26 |
27 | function getOpenedGroups() {
28 | const settings = getLocalSettings();
29 |
30 | return settings.openedGroups ?? [];
31 | }
32 |
33 | function toggleOpenedGroup(group) {
34 | let openedGroups = getOpenedGroups();
35 |
36 | const index = openedGroups.indexOf(group);
37 |
38 | if (index === -1) {
39 | openedGroups.push(group);
40 | } else {
41 | openedGroups.splice(index, 1);
42 | }
43 |
44 | setLocalSettings('openedGroups', openedGroups);
45 |
46 | return openedGroups;
47 | }
48 |
49 | function sidebarIsOpen() {
50 | const settings = getLocalSettings();
51 |
52 | return settings.sidebarIsOpen ?? true;
53 | }
54 |
55 | function toggleSidebar() {
56 | let showSidebar = sidebarIsOpen();
57 |
58 | showSidebar = !showSidebar;
59 |
60 | setLocalSettings('sidebarIsOpen', showSidebar);
61 |
62 | return showSidebar;
63 | }
64 |
65 | function getTheme() {
66 | const settings = getLocalSettings();
67 |
68 | return settings.theme ?? '';
69 | }
70 |
71 | function setTheme(theme) {
72 | setLocalSettings('theme', theme);
73 | }
74 |
75 | export {getOpenedGroups, toggleOpenedGroup, sidebarIsOpen, toggleSidebar, getTheme, setTheme};
76 |
--------------------------------------------------------------------------------
/app/Actions/Fortify/UpdateUserProfileInformation.php:
--------------------------------------------------------------------------------
1 | ['required', 'string', 'max:255'],
23 | 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
24 | 'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
25 | ])->validateWithBag('updateProfileInformation');
26 |
27 | if (isset($input['photo'])) {
28 | $user->updateProfilePhoto($input['photo']);
29 | }
30 |
31 | if ($input['email'] !== $user->email &&
32 | $user instanceof MustVerifyEmail) {
33 | $this->updateVerifiedUser($user, $input);
34 | } else {
35 | $user->forceFill([
36 | 'name' => $input['name'],
37 | 'email' => $input['email'],
38 | ])->save();
39 | }
40 | }
41 |
42 | /**
43 | * Update the given verified user's profile information.
44 | *
45 | * @param mixed $user
46 | * @param array $input
47 | * @return void
48 | */
49 | protected function updateVerifiedUser($user, array $input)
50 | {
51 | $user->forceFill([
52 | 'name' => $input['name'],
53 | 'email' => $input['email'],
54 | 'email_verified_at' => null,
55 | ])->save();
56 |
57 | $user->sendEmailVerificationNotification();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/database/factories/UserFactory.php:
--------------------------------------------------------------------------------
1 | $this->faker->name(),
29 | 'email' => $this->faker->unique()->safeEmail(),
30 | 'email_verified_at' => now(),
31 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
32 | 'remember_token' => Str::random(10),
33 | ];
34 | }
35 |
36 | /**
37 | * Indicate that the model's email address should be unverified.
38 | *
39 | * @return \Illuminate\Database\Eloquent\Factories\Factory
40 | */
41 | public function unverified()
42 | {
43 | return $this->state(function (array $attributes) {
44 | return [
45 | 'email_verified_at' => null,
46 | ];
47 | });
48 | }
49 |
50 | /**
51 | * Indicate that the user should have a personal team.
52 | *
53 | * @return $this
54 | */
55 | public function withPersonalTeam()
56 | {
57 | if (! Features::hasTeamFeatures()) {
58 | return $this->state([]);
59 | }
60 |
61 | return $this->has(
62 | Team::factory()
63 | ->state(function (array $attributes, User $user) {
64 | return ['name' => $user->name.'\'s Team', 'user_id' => $user->id, 'personal_team' => true];
65 | }),
66 | 'ownedTeams'
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/resources/js/Pages/Auth/ConfirmPassword.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
31 |
32 |
33 | {#snippet logo()}
34 |
35 | {/snippet}
36 |
37 |
38 | This is a secure area of the application. Please confirm your password before continuing.
39 |
40 |
41 |
42 |
43 |
57 |
58 |
--------------------------------------------------------------------------------
/app/Console/Commands/DeleteDuplicateLinksCommand.php:
--------------------------------------------------------------------------------
1 | ask('User ID');
33 |
34 | try {
35 | Validator::make(['id' => $userId], [
36 | 'id' => ['required', 'numeric'],
37 | ])->validate();
38 | } catch (ValidationException $exception) {
39 | $this->error($exception->getMessage());
40 | return Command::FAILURE;
41 | }
42 |
43 | $user = User::find($userId);
44 |
45 | if (empty($user)) {
46 | $this->error('The user does not exist.');
47 |
48 | return Command::FAILURE;
49 | }
50 |
51 | $duplicates = $linkService->getDuplicates($user);
52 |
53 | if (count($duplicates) === 0) {
54 | $this->info('No duplicates found.');
55 |
56 | return Command::SUCCESS;
57 | }
58 |
59 | if ($this->confirm(count($duplicates) . ' links found. Do you want to delete them?')) {
60 | $linkService->deleteDuplicates($duplicates);
61 |
62 | $this->info('Duplicate links deleted.');
63 |
64 | return Command::SUCCESS;
65 | }
66 |
67 | $this->error('Please confirm the deletion.');
68 |
69 | return Command::FAILURE;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/resources/js/Pages/Auth/Login.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
33 |
34 |
35 |
57 |
58 |
--------------------------------------------------------------------------------
/resources/js/Pages/Inbox/Index.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
55 |
56 |
57 |
58 | {#snippet toolbar()}
59 |
60 | toggleUntagged(value)} toggled={untagged} title="Untagged"/>
61 | toggleUngrouped(value)} toggled={ungrouped} title="Ungrouped"/>
62 |
63 | {/snippet}
64 |
65 |
66 |
--------------------------------------------------------------------------------
/tests/Feature/EmailVerificationTest.php:
--------------------------------------------------------------------------------
1 | withPersonalTeam()->create([
12 | 'email_verified_at' => null,
13 | ]);
14 |
15 | $response = $this->actingAs($user)->get('/email/verify');
16 |
17 | $response->assertStatus(200);
18 | })->skip(function () {
19 | return ! Features::enabled(Features::emailVerification());
20 | }, 'Email verification not enabled.');
21 |
22 | test('email can be verified', function () {
23 | Event::fake();
24 |
25 | $user = User::factory()->create([
26 | 'email_verified_at' => null,
27 | ]);
28 |
29 | $verificationUrl = URL::temporarySignedRoute(
30 | 'verification.verify',
31 | now()->addMinutes(60),
32 | ['id' => $user->id, 'hash' => sha1($user->email)]
33 | );
34 |
35 | $response = $this->actingAs($user)->get($verificationUrl);
36 |
37 | Event::assertDispatched(Verified::class);
38 |
39 | expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
40 | $response->assertRedirect(RouteServiceProvider::HOME.'?verified=1');
41 | })->skip(function () {
42 | return ! Features::enabled(Features::emailVerification());
43 | }, 'Email verification not enabled.');
44 |
45 | test('email can not verified with invalid hash', function () {
46 | $user = User::factory()->create([
47 | 'email_verified_at' => null,
48 | ]);
49 |
50 | $verificationUrl = URL::temporarySignedRoute(
51 | 'verification.verify',
52 | now()->addMinutes(60),
53 | ['id' => $user->id, 'hash' => sha1('wrong-email')]
54 | );
55 |
56 | $this->actingAs($user)->get($verificationUrl);
57 |
58 | expect($user->fresh()->hasVerifiedEmail())->toBeFalse();
59 | })->skip(function () {
60 | return ! Features::enabled(Features::emailVerification());
61 | }, 'Email verification not enabled.');
62 |
--------------------------------------------------------------------------------
/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 | 'client_options' => [
43 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
44 | ],
45 | ],
46 |
47 | 'ably' => [
48 | 'driver' => 'ably',
49 | 'key' => env('ABLY_KEY'),
50 | ],
51 |
52 | 'redis' => [
53 | 'driver' => 'redis',
54 | 'connection' => 'default',
55 | ],
56 |
57 | 'log' => [
58 | 'driver' => 'log',
59 | ],
60 |
61 | 'null' => [
62 | 'driver' => 'null',
63 | ],
64 |
65 | ],
66 |
67 | ];
68 |
--------------------------------------------------------------------------------
/resources/js/Pages/Auth/VerifyEmail.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
28 |
29 |
30 | {#snippet logo()}
31 |
32 | {/snippet}
33 |
34 |
35 | Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we
36 | just emailed to you? If you didn't receive the email, we will gladly send you another.
37 |
38 |
39 | {#if verificationLinkSent}
40 |
41 | A new verification link has been sent to the email address you provided during registration.
42 |
43 | {/if}
44 |
45 |
57 |
58 |
--------------------------------------------------------------------------------