├── .all-contributorsrc ├── .commitlintrc.json ├── .editorconfig ├── .env.ci ├── .env.example ├── .env.test ├── .gitattributes ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .tool-versions ├── LICENSE ├── README.md ├── app ├── Enums │ ├── Domain │ │ └── Status.php │ ├── Link │ │ └── Os.php │ ├── LinkStat │ │ └── Event.php │ ├── Media │ │ └── Visibility.php │ ├── Tag │ │ └── Color.php │ └── User │ │ ├── Role.php │ │ └── Theme.php ├── Http │ ├── Controllers │ │ ├── AnalyticsController.php │ │ ├── Api │ │ │ ├── Controller.php │ │ │ ├── DomainController.php │ │ │ ├── LinkController.php │ │ │ ├── QrcodeController.php │ │ │ ├── TagController.php │ │ │ └── WebsiteController.php │ │ ├── Auth │ │ │ ├── AuthenticatedSessionController.php │ │ │ ├── ConfirmablePasswordController.php │ │ │ ├── Controller.php │ │ │ ├── EmailVerificationNotificationController.php │ │ │ ├── EmailVerificationPromptController.php │ │ │ ├── GoogleController.php │ │ │ ├── InviteController.php │ │ │ ├── NewPasswordController.php │ │ │ ├── PasswordController.php │ │ │ ├── PasswordResetLinkController.php │ │ │ ├── RegisteredUserController.php │ │ │ └── VerifyEmailController.php │ │ ├── Controller.php │ │ ├── EventController.php │ │ ├── LinkController.php │ │ ├── MediaController.php │ │ ├── RedirectController.php │ │ ├── Setting │ │ │ ├── AccountController.php │ │ │ ├── ApiTokenController.php │ │ │ ├── BillingController.php │ │ │ ├── Controller.php │ │ │ ├── DomainController.php │ │ │ ├── InviteController.php │ │ │ ├── TagController.php │ │ │ ├── TeamMemberController.php │ │ │ ├── UsageController.php │ │ │ └── WorkspaceController.php │ │ └── WorkspaceController.php │ ├── Middleware │ │ ├── Api │ │ │ └── Auth.php │ │ ├── Billing.php │ │ ├── CustomDomain.php │ │ ├── HandleInertiaRequests.php │ │ └── SetWorkspace.php │ ├── Requests │ │ ├── Account │ │ │ └── UpdateRequest.php │ │ ├── Auth │ │ │ └── LoginRequest.php │ │ ├── Domain │ │ │ ├── CreateRequest.php │ │ │ └── UpdateRequest.php │ │ ├── Invite │ │ │ └── InviteRequest.php │ │ ├── Link │ │ │ ├── CreateRequest.php │ │ │ └── UpdateRequest.php │ │ ├── ProfileUpdateRequest.php │ │ ├── Tag │ │ │ ├── CreateRequest.php │ │ │ └── UpdateRequest.php │ │ ├── TeamMember │ │ │ └── UpdateUserRoleRequest.php │ │ └── Workspace │ │ │ ├── CreateRequest.php │ │ │ └── UpdateRequest.php │ └── Resources │ │ └── Api │ │ ├── DomainResource.php │ │ ├── LinkResource.php │ │ └── TagResource.php ├── Jobs │ └── ProcessLinkStat.php ├── Listeners │ └── StripeEventListener.php ├── Mail │ └── Team │ │ └── SendUserInvite.php ├── Models │ ├── ApiToken.php │ ├── Domain.php │ ├── Invite.php │ ├── Link.php │ ├── LinkStat.php │ ├── Media.php │ ├── Plan.php │ ├── Scopes │ │ ├── MediaScope.php │ │ └── TagScope.php │ ├── Tag.php │ ├── Traits │ │ ├── HasWorkspaces.php │ │ └── WorkspaceUsage.php │ ├── User.php │ └── Workspace.php ├── Observers │ ├── DomainObserver.php │ └── WorkspaceObserver.php ├── Policies │ └── WorkspacePolicy.php ├── Providers │ ├── AppServiceProvider.php │ ├── HorizonServiceProvider.php │ └── TelescopeServiceProvider.php └── Services │ ├── CalculateStat.php │ ├── MediaUrlGeneratorService.php │ └── UserAgentService.php ├── artisan ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── broadcasting.php ├── cache.php ├── cashier.php ├── cors.php ├── database.php ├── domains.php ├── filesystems.php ├── geoip.php ├── horizon.php ├── logging.php ├── mail.php ├── media-library.php ├── pennant.php ├── queue.php ├── reverb.php ├── sentry.php ├── services.php ├── session.php └── telescope.php ├── database ├── .gitignore ├── factories │ ├── ApiTokenFactory.php │ ├── DomainFactory.php │ ├── InviteFactory.php │ ├── LinkFactory.php │ ├── LinkStatFactory.php │ ├── PlanFactory.php │ ├── TagFactory.php │ ├── UserFactory.php │ ├── WorkspaceFactory.php │ └── data │ │ ├── languages.json │ │ └── userAgent.json ├── migrations │ ├── 0001_01_01_000000_create_users_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 0001_01_01_000002_create_jobs_table.php │ ├── 2023_05_05_001123_create_plans_table.php │ ├── 2023_05_05_001124_create_workspaces_table.php │ ├── 2023_05_05_001144_create_user_workspace_table.php │ ├── 2023_05_06_230755_create_invites_table.php │ ├── 2024_10_18_031325_create_domains_table.php │ ├── 2024_10_18_031326_create_links_table.php │ ├── 2024_10_18_032933_create_subscriptions_table.php │ ├── 2024_10_18_032934_create_subscription_items_table.php │ ├── 2024_10_18_034126_create_link_stats_table.php │ ├── 2024_10_18_210819_create_medias_table.php │ ├── 2024_10_18_212443_create_telescope_entries_table.php │ ├── 2024_10_19_170336_create_tags_table.php │ ├── 2024_10_19_183425_create_link_tag_table.php │ ├── 2024_10_19_191838_create_api_tokens_table.php │ ├── 2024_12_02_215230_add_ios_and_android_to_links_table.php │ └── 2025_01_26_165050_add_expires_at_to_links_table.php └── seeders │ ├── DatabaseSeeder.php │ ├── LocalDevelopmentSeeder.php │ └── PlanSeeder.php ├── docker-compose.yml ├── jsconfig.json ├── lang └── en │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── lua.sh.postman_collection.json ├── maizzle ├── .editorconfig ├── .github │ ├── FUNDING.yml │ ├── dependabot.yml │ ├── logo-dark.svg │ └── logo-light.svg ├── .gitignore ├── .tool-versions ├── LICENSE ├── README.md ├── config.js ├── config.production.js ├── package-lock.json ├── package.json ├── src │ ├── components │ │ ├── button.html │ │ ├── divider.html │ │ ├── footer.html │ │ ├── header.html │ │ ├── spacer.html │ │ ├── v-fill.html │ │ └── v-image.html │ ├── css │ │ ├── resets.css │ │ ├── tailwind.css │ │ └── utilities.css │ ├── images │ │ └── maizzle.png │ ├── layouts │ │ └── main.html │ └── templates │ │ ├── email-verification.html │ │ ├── invite.html │ │ ├── promotional.html │ │ └── transactional.html └── tailwind.config.js ├── package-lock.json ├── package.json ├── phpunit.xml ├── postcss.config.js ├── public ├── .DS_Store ├── .htaccess ├── favicon.ico ├── images │ ├── links │ │ └── qr-base.png │ ├── lua │ │ ├── full-black.svg │ │ ├── full-color.svg │ │ ├── full-white.svg │ │ ├── logo-black.svg │ │ ├── logo-color.svg │ │ └── logo-white.svg │ ├── user │ │ └── avatar.jpg │ └── websites │ │ └── favicon.png ├── index.php ├── robots.txt └── vendor │ └── telescope │ ├── app-dark.css │ ├── app.css │ ├── app.js │ ├── favicon.ico │ └── mix-manifest.json ├── resources ├── css │ ├── app.css │ ├── floating.css │ └── v-calendar.css ├── js │ ├── Components │ │ ├── Accordion.vue │ │ ├── ActionSection.vue │ │ ├── Announcement.vue │ │ ├── ApplicationLogo.vue │ │ ├── Banner.vue │ │ ├── Button.vue │ │ ├── Chart.vue │ │ ├── Checkbox.vue │ │ ├── ColorSelector.vue │ │ ├── ConfirmDeleteModal.vue │ │ ├── ConfirmationModal.vue │ │ ├── DatePicker.vue │ │ ├── DialogModal.vue │ │ ├── DomainStatus.vue │ │ ├── Dropdown.vue │ │ ├── EmptyState.vue │ │ ├── FormSection.vue │ │ ├── Input.vue │ │ ├── InputError.vue │ │ ├── InputHelp.vue │ │ ├── Label.vue │ │ ├── Modal.vue │ │ ├── Pagination.vue │ │ ├── Qrcode.vue │ │ ├── Radio.vue │ │ ├── RangePicker.vue │ │ ├── Sandbox.vue │ │ ├── SectionBorder.vue │ │ ├── SectionTitle.vue │ │ ├── Select.vue │ │ ├── SlideOver.vue │ │ ├── Tab.vue │ │ ├── Table.vue │ │ ├── Tag.vue │ │ ├── Textarea.vue │ │ ├── Toggle.vue │ │ └── UserAvatar.vue │ ├── Layouts │ │ ├── Auth.vue │ │ ├── Components │ │ │ ├── Menu.vue │ │ │ ├── Usage.vue │ │ │ └── UserDropdown.vue │ │ ├── Master.vue │ │ └── Sidebar.vue │ ├── Pages │ │ ├── Analytics │ │ │ ├── Device │ │ │ │ ├── Browser.vue │ │ │ │ ├── Device.vue │ │ │ │ ├── Index.vue │ │ │ │ ├── Language.vue │ │ │ │ └── Os.vue │ │ │ ├── Event │ │ │ │ └── Index.vue │ │ │ ├── Index.vue │ │ │ ├── Link │ │ │ │ └── Index.vue │ │ │ ├── Location │ │ │ │ ├── City.vue │ │ │ │ ├── Country.vue │ │ │ │ ├── Index.vue │ │ │ │ └── Region.vue │ │ │ ├── NoDataPlaceholder.vue │ │ │ └── Source │ │ │ │ ├── Campaign.vue │ │ │ │ ├── Content.vue │ │ │ │ ├── Index.vue │ │ │ │ ├── Medium.vue │ │ │ │ ├── Referer.vue │ │ │ │ ├── Source.vue │ │ │ │ └── Term.vue │ │ ├── Auth │ │ │ ├── ConfirmPassword.vue │ │ │ ├── ForgotPassword.vue │ │ │ ├── Invitation.vue │ │ │ ├── Login.vue │ │ │ ├── Partial │ │ │ │ └── Social.vue │ │ │ ├── Register.vue │ │ │ ├── ResetPassword.vue │ │ │ └── VerifyEmail.vue │ │ ├── Error.vue │ │ ├── Event │ │ │ ├── ChartClick.vue │ │ │ ├── ChartQR.vue │ │ │ ├── Header.vue │ │ │ └── Index.vue │ │ ├── Link │ │ │ ├── Create.vue │ │ │ ├── Edit.vue │ │ │ ├── Index.vue │ │ │ └── Password.vue │ │ ├── Setting │ │ │ ├── Account │ │ │ │ ├── Avatar.vue │ │ │ │ └── Edit.vue │ │ │ ├── ApiToken │ │ │ │ ├── Create.vue │ │ │ │ └── Index.vue │ │ │ ├── Billing │ │ │ │ ├── Index.vue │ │ │ │ ├── Success.vue │ │ │ │ └── Upgrade.vue │ │ │ ├── Domain │ │ │ │ ├── Create.vue │ │ │ │ ├── Edit.vue │ │ │ │ └── Index.vue │ │ │ ├── Tag │ │ │ │ ├── Create.vue │ │ │ │ ├── Edit.vue │ │ │ │ └── Index.vue │ │ │ ├── TeamMember │ │ │ │ ├── Index.vue │ │ │ │ └── Invite │ │ │ │ │ ├── Create.vue │ │ │ │ │ └── Index.vue │ │ │ └── Workspace │ │ │ │ ├── Edit.vue │ │ │ │ └── Logo.vue │ │ └── Workspace │ │ │ └── Create.vue │ ├── app.js │ ├── bootstrap.js │ ├── country.js │ ├── date.js │ ├── dayjs.js │ ├── debounce.js │ ├── echo.js │ ├── helper.js │ └── theme.js └── views │ ├── app.blade.php │ └── mail │ ├── email-verification.blade.php │ ├── invite.blade.php │ ├── promotional.blade.php │ └── transactional.blade.php ├── routes ├── api.php ├── auth.php ├── channels.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ ├── private │ │ └── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.js ├── tests ├── Feature │ ├── Api │ │ ├── DomainTest.php │ │ ├── LinkTest.php │ │ ├── TagTest.php │ │ └── WebsiteTest.php │ ├── App │ │ ├── AccountTest.php │ │ ├── AnalyticsTest.php │ │ ├── ApiTokenTest.php │ │ ├── BillingTest.php │ │ ├── DomainTest.php │ │ ├── EventTest.php │ │ ├── LinkTest.php │ │ ├── RedirectTest.php │ │ ├── TagTest.php │ │ ├── UserTest.php │ │ └── WorkspaceTest.php │ └── Auth │ │ ├── AuthenticationTest.php │ │ ├── EmailVerificationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── PasswordResetTest.php │ │ └── RegistrationTest.php ├── Pest.php ├── TestCase.php └── Unit │ └── ExampleTest.php └── vite.config.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "lua", 3 | "projectOwner": "luadotsh", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "contributorsPerLine": 7, 11 | "contributorsSortAlphabetically": false, 12 | "linkToUsage": true, 13 | "skipCi": true, 14 | "contributors": [ 15 | { 16 | "login": "paulocastellano", 17 | "name": "Paulo Castellano", 18 | "avatar_url": "https://avatars.githubusercontent.com/u/265964?v=4", 19 | "profile": "https://github.com/paulocastellano", 20 | "contributions": [ 21 | "code" 22 | ] 23 | }, 24 | { 25 | "login": "Arkanius", 26 | "name": "Victor Gazotti", 27 | "avatar_url": "https://avatars.githubusercontent.com/u/6404401?v=4", 28 | "profile": "https://conferencias.dev/", 29 | "contributions": [ 30 | "code" 31 | ] 32 | }, 33 | { 34 | "login": "MuhammadSaim", 35 | "name": "Muhammad Saim", 36 | "avatar_url": "https://avatars.githubusercontent.com/u/19898499?v=4", 37 | "profile": "https://muhammadsaim.com", 38 | "contributions": [ 39 | "code" 40 | ] 41 | }, 42 | { 43 | "login": "IbonAzkoitia", 44 | "name": "Ibon Azkoitia", 45 | "avatar_url": "https://avatars.githubusercontent.com/u/6384282?v=4", 46 | "profile": "https://promium.pro", 47 | "contributions": [ 48 | "code" 49 | ] 50 | } 51 | ], 52 | "commitConvention": "angular", 53 | "commitType": "docs" 54 | } 55 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": { 4 | "type-enum": [2, "always", ["chore", "fix", "feat"]], 5 | "subject-case": [ 6 | 2, 7 | "never", 8 | ["sentence-case", "start-case", "pascal-case", "upper-case"] 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.ci: -------------------------------------------------------------------------------- 1 | APP_ENV=testing 2 | APP_URL=https://lua.sh 3 | APP_KEY=base64:Zfo+2dkSaHvem5LCiZS/baYHv2Pv1vrSc0F2NaF29Ec= 4 | 5 | LOG_CHANNEL=stack 6 | LOG_DEPRECATIONS_CHANNEL=null 7 | LOG_LEVEL=debug 8 | 9 | BROADCAST_DRIVER=log 10 | CACHE_DRIVER=file 11 | FILESYSTEM_DISK=local 12 | QUEUE_CONNECTION=sync 13 | SESSION_DRIVER=file 14 | SESSION_LIFETIME=1440 15 | 16 | MAIL_MAILER=log 17 | 18 | # Stripe 19 | STRIPE_KEY= 20 | STRIPE_SECRET= 21 | STRIPE_WEBHOOK_SECRET= 22 | 23 | # Sentry 24 | SENTRY_LARAVEL_DSN= 25 | SENTRY_TRACES_SAMPLE_RATE=1.0 26 | SENTRY_AUTH_TOKEN= 27 | SENTRY_ENVIRONMENT= 28 | 29 | TELESCOPE_ENABLED=false 30 | 31 | # Vite 32 | VITE_STRIPE_KEY="${STRIPE_KEY}" 33 | VITE_SENTRY_AUTH_TOKEN="${SENTRY_AUTH_TOKEN}" 34 | VITE_SENTRY_DSN_PUBLIC="${SENTRY_LARAVEL_DSN}" 35 | VITE_SENTRY_ENVIRONMENT="${SENTRY_ENVIRONMENT}" 36 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 37 | VITE_PUSHER_HOST="${PUSHER_HOST}" 38 | VITE_PUSHER_PORT="${PUSHER_PORT}" 39 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 40 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 41 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://localhost 7 | 8 | APP_LOCALE=en 9 | APP_FALLBACK_LOCALE=en 10 | APP_FAKER_LOCALE=en_US 11 | 12 | APP_MAINTENANCE_DRIVER=file 13 | # APP_MAINTENANCE_STORE=database 14 | 15 | PHP_CLI_SERVER_WORKERS=4 16 | 17 | BCRYPT_ROUNDS=12 18 | 19 | LOG_CHANNEL=stack 20 | LOG_STACK=single 21 | LOG_DEPRECATIONS_CHANNEL=null 22 | LOG_LEVEL=debug 23 | 24 | DB_CONNECTION=mysql 25 | DB_HOST=127.0.0.1 26 | DB_PORT=3306 27 | DB_DATABASE=lua.sh 28 | DB_USERNAME=root 29 | DB_PASSWORD= 30 | 31 | SESSION_DRIVER=database 32 | SESSION_LIFETIME=120 33 | SESSION_ENCRYPT=false 34 | SESSION_PATH=/ 35 | SESSION_DOMAIN=null 36 | 37 | BROADCAST_CONNECTION=log 38 | FILESYSTEM_DISK=local 39 | QUEUE_CONNECTION=database 40 | 41 | CACHE_STORE=database 42 | CACHE_PREFIX= 43 | 44 | MEMCACHED_HOST=127.0.0.1 45 | 46 | REDIS_CLIENT=phpredis 47 | REDIS_HOST=127.0.0.1 48 | REDIS_PASSWORD=null 49 | REDIS_PORT=6379 50 | 51 | RESEND_API_KEY= 52 | 53 | MAIL_MAILER=log 54 | MAIL_HOST=127.0.0.1 55 | MAIL_PORT=2525 56 | MAIL_USERNAME=null 57 | MAIL_PASSWORD=null 58 | MAIL_ENCRYPTION=null 59 | MAIL_FROM_ADDRESS="hello@example.com" 60 | MAIL_FROM_NAME="${APP_NAME}" 61 | 62 | AWS_ACCESS_KEY_ID= 63 | AWS_SECRET_ACCESS_KEY= 64 | AWS_DEFAULT_REGION=us-east-1 65 | AWS_BUCKET= 66 | AWS_USE_PATH_STYLE_ENDPOINT=false 67 | 68 | SENTRY_LARAVEL_DSN= 69 | SENTRY_TRACES_SAMPLE_RATE=1.0 70 | 71 | VITE_APP_NAME="${APP_NAME}" 72 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | DB_DATABASE=luash_test 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /storage/pail 8 | /vendor 9 | .env 10 | .env.backup 11 | .env.production 12 | .phpactor.json 13 | .phpunit.result.cache 14 | Homestead.json 15 | Homestead.yaml 16 | auth.json 17 | npm-debug.log 18 | yarn-error.log 19 | /.fleet 20 | /.idea 21 | /.vscode 22 | /.zed 23 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit "$1" 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | 2 | # php artisan test 3 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.18.1 2 | -------------------------------------------------------------------------------- /app/Enums/Domain/Status.php: -------------------------------------------------------------------------------- 1 | validate([ 22 | 'download' => ['nullable', 'boolean'], 23 | 'color' => ['nullable', 'regex:/^#([a-f0-9]{6}|[a-f0-9]{3})$/i'], 24 | ]); 25 | 26 | $link = Link::findOrFail($id); 27 | 28 | $qrCodeGenerator = QrCode::getFacadeRoot(); 29 | 30 | $rgb = Hex::fromString($request->query('color') ? $request->query('color') : '#000000')->toRgb(); 31 | $bgColor = [$rgb->red(), $rgb->green(), $rgb->blue(), 100]; 32 | 33 | $qrCode = $qrCodeGenerator 34 | ->size(256) 35 | ->format('png') 36 | ->backgroundColor(...$bgColor) 37 | ->color(255, 255, 255, 100) 38 | // ->merge('/public/images/links/qr-base.png') 39 | ->errorCorrection('M') 40 | ->generate("{$link->link}?qr=1"); 41 | 42 | // download qr code 43 | if ($request->query('download') == true) { 44 | return response()->streamDownload( 45 | function () use ($qrCode): void { 46 | /** @var string $qrCode */ 47 | echo $qrCode; 48 | }, 49 | 'qr-code.png', 50 | ['Content-Type' => 'image/png'] 51 | ); 52 | } 53 | 54 | // render qr code 55 | return response($qrCode) 56 | ->header('Content-Type', 'image/png'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/WebsiteController.php: -------------------------------------------------------------------------------- 1 | url}&size=128"); 15 | } catch (\Throwable $th) { 16 | $favicon = file_get_contents(public_path('/images/websites/favicon.png')); 17 | } 18 | 19 | return response($favicon)->header('Content-Type', 'image/png'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | Route::has('password.request'), 24 | 'status' => session('status'), 25 | ]); 26 | } 27 | 28 | /** 29 | * Handle an incoming authentication request. 30 | */ 31 | public function store(LoginRequest $request): RedirectResponse 32 | { 33 | $request->authenticate(); 34 | 35 | $request->session()->regenerate(); 36 | 37 | return redirect()->intended(route('links.index', absolute: false)); 38 | } 39 | 40 | /** 41 | * Destroy an authenticated session. 42 | */ 43 | public function destroy(Request $request): RedirectResponse 44 | { 45 | Auth::guard('web')->logout(); 46 | 47 | $request->session()->invalidate(); 48 | 49 | $request->session()->regenerateToken(); 50 | 51 | return redirect('/'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 30 | 'email' => $request->user()->email, 31 | 'password' => $request->password, 32 | ])) { 33 | throw ValidationException::withMessages([ 34 | 'password' => __('auth.password'), 35 | ]); 36 | } 37 | 38 | $request->session()->put('auth.password_confirmed_at', time()); 39 | 40 | return redirect()->intended(route('links.index', absolute: false)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/Controller.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 18 | return redirect()->intended(route('links.index', absolute: false)); 19 | } 20 | 21 | $request->user()->sendEmailVerificationNotification(); 22 | 23 | return back()->with('status', 'verification-link-sent'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail() 20 | ? redirect()->intended(route('links.index', absolute: false)) 21 | : Inertia::render('Auth/VerifyEmail', ['status' => session('status')]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/GoogleController.php: -------------------------------------------------------------------------------- 1 | redirect()); 24 | } 25 | 26 | /** 27 | * Obtain the user information from Google. 28 | * 29 | * @return \Illuminate\Http\RedirectResponse 30 | */ 31 | public function handleProviderCallback(Request $request) 32 | { 33 | try { 34 | $googleUser = Socialite::driver('google')->user(); 35 | } catch (\Exception $e) { 36 | return redirect(route('login')); 37 | } 38 | 39 | // check if they're an existing user 40 | $existingUser = User::where('email', $googleUser->email)->first(); 41 | if ($existingUser) { 42 | 43 | // log them in 44 | auth()->login($existingUser, true); 45 | 46 | return redirect()->to(route('links.index')); 47 | } 48 | 49 | // create user 50 | $user = User::create([ 51 | 'name' => $googleUser->name, 52 | 'email' => $googleUser->email, 53 | 'email_verified_at' => now(), 54 | ]); 55 | 56 | event(new Registered($user)); 57 | 58 | Auth::login($user); 59 | 60 | return redirect(route('workspaces.create')); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 21 | 'current_password' => ['required', 'current_password'], 22 | 'password' => ['required', Password::defaults(), 'confirmed'], 23 | ]); 24 | 25 | $request->user()->update([ 26 | 'password' => Hash::make($validated['password']), 27 | ]); 28 | 29 | return back(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | session('status'), 24 | ]); 25 | } 26 | 27 | /** 28 | * Handle an incoming password reset link request. 29 | * 30 | * @throws \Illuminate\Validation\ValidationException 31 | */ 32 | public function store(Request $request): RedirectResponse 33 | { 34 | $request->validate([ 35 | 'email' => 'required|email', 36 | ]); 37 | 38 | // We will send the password reset link to this user. Once we have attempted 39 | // to send the link, we will examine the response then see the message we 40 | // need to show to the user. Finally, we'll send out a proper response. 41 | $status = Password::sendResetLink( 42 | $request->only('email') 43 | ); 44 | 45 | if ($status == Password::RESET_LINK_SENT) { 46 | return back()->with('status', __($status)); 47 | } 48 | 49 | throw ValidationException::withMessages([ 50 | 'email' => [trans($status)], 51 | ]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisteredUserController.php: -------------------------------------------------------------------------------- 1 | validate([ 38 | 'name' => 'required|string|max:255', 39 | 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class, 40 | 'password' => ['required', Rules\Password::defaults()], 41 | ]); 42 | 43 | $user = User::create([ 44 | 'name' => $request->name, 45 | 'email' => $request->email, 46 | 'password' => Hash::make($request->password), 47 | ]); 48 | 49 | event(new Registered($user)); 50 | 51 | Auth::login($user); 52 | 53 | return redirect(route('workspaces.create', absolute: false)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/VerifyEmailController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 20 | return redirect()->intended(route('links.index', absolute: false).'?verified=1'); 21 | } 22 | 23 | if ($request->user()->markEmailAsVerified()) { 24 | event(new Verified($request->user())); 25 | } 26 | 27 | return redirect()->intended(route('links.index', absolute: false).'?verified=1'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | user()->currentWorkspace; 20 | 21 | $start = Carbon::createFromFormat('Y-m-d', $request->start ? $request->start : now()->subDays(30)->format('Y-m-d'))->startOfDay(); 22 | $end = Carbon::createFromFormat('Y-m-d', $request->end ? $request->end : now()->format('Y-m-d'))->endOfDay(); 23 | 24 | $query = LinkStat::where('workspace_id', $workspace->id) 25 | ->with('link:id,link') 26 | ->whereBetween('created_at', [$start, $end]) 27 | ->latest(); 28 | 29 | $links = $query->paginate(config('app.pagination.default'))->withQueryString(); 30 | 31 | return Inertia::render('Event/Index', [ 32 | 'table' => $links, 33 | 'hasData' => LinkStat::where('workspace_id', $workspace->id)->exists(), 34 | 'start' => $start->format('Y-m-d'), 35 | 'end' => $end->format('Y-m-d'), 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Controllers/Setting/ApiTokenController.php: -------------------------------------------------------------------------------- 1 | user()->current_workspace_id)->get(); 20 | 21 | return Inertia::render('Setting/ApiToken/Index', [ 22 | 'tokens' => $tokens, 23 | 'hasData' => $tokens->count() === 0 ? false : true 24 | ]); 25 | } 26 | 27 | public function store(Request $request) 28 | { 29 | $request->validate([ 30 | 'name' => 'required|string' 31 | ]); 32 | 33 | $token = ApiToken::create([ 34 | 'workspace_id' => auth()->user()->current_workspace_id, 35 | 'name' => $request->name, 36 | 'token' => Str::uuid() 37 | ]); 38 | 39 | return back()->with('flash', [ 40 | 'token' => $token->token 41 | ]); 42 | } 43 | 44 | public function destroy($id) 45 | { 46 | $token = ApiToken::where('workspace_id', auth()->user()->current_workspace_id)->findOrFail($id); 47 | $token->delete(); 48 | 49 | session()->flash('flash.banner', 'API Token deleted successful.'); 50 | session()->flash('flash.bannerStyle', 'success'); 51 | 52 | return back(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Http/Controllers/Setting/BillingController.php: -------------------------------------------------------------------------------- 1 | where('is_private', false)->get(); 24 | return Inertia::render('Setting/Billing/Upgrade', [ 25 | 'plans' => $plans, 26 | ]); 27 | } 28 | 29 | public function checkout($planId, Request $request) 30 | { 31 | $workspace = $request->user()->currentWorkspace; 32 | 33 | // get the plan 34 | $plan = Plan::where('id', $planId)->firstOrFail(); 35 | 36 | // create a stripe customer 37 | $workspace->createOrGetStripeCustomer(); 38 | 39 | return $workspace 40 | ->newSubscription('default', $plan->stripe_id) 41 | ->allowPromotionCodes() 42 | ->checkout([ 43 | 'success_url' => route('setting.billing.checkout-success'), 44 | 'cancel_url' => route('setting.billing.upgrade'), 45 | ]); 46 | } 47 | 48 | public function billingPortal(Request $request) 49 | { 50 | return Inertia::location($request->user()->currentWorkspace->redirectToBillingPortal(route('setting.billing.index'))); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Http/Controllers/Setting/Controller.php: -------------------------------------------------------------------------------- 1 | user()->currentWorkspace; 23 | 24 | $workspace->update([ 25 | 'name' => $request->name, 26 | ]); 27 | 28 | session()->flash('flash.banner', 'Workspace updated'); 29 | session()->flash('flash.bannerStyle', 'success'); 30 | 31 | return back(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Middleware/Api/Auth.php: -------------------------------------------------------------------------------- 1 | header('authorization') ? $request->bearerToken() : null; 26 | if(!$token) { 27 | return response()->json(['status' => 'error', 'message' => 'Token not provided'], 401); 28 | } 29 | 30 | // get the token 31 | $token = ApiToken::where('token', $token)->with('workspace')->first(); 32 | if (!$token) { 33 | return response()->json(['status' => 'error', 'message' => 'Invalid token'], 401); 34 | } 35 | 36 | // update the last used time 37 | $token->update([ 38 | 'last_used_at' => now(), 39 | ]); 40 | 41 | $request->merge([ 42 | 'workspace' => $token->workspace, 43 | ]); 44 | 45 | return $next($request); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Http/Middleware/Billing.php: -------------------------------------------------------------------------------- 1 | user()->currentWorkspace->usage()); 16 | return $next($request); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Middleware/CustomDomain.php: -------------------------------------------------------------------------------- 1 | key) { 26 | return $next($request); 27 | } 28 | 29 | // Get the host from the request. 30 | $host = $request->getHost(); 31 | 32 | // If the domain is provided by lua, we redirect to the website. 33 | if(in_array($host, config('domains.available'))) { 34 | return Inertia::location(config('app.website')); 35 | } 36 | 37 | // Check if the domain exists in the database and is active. 38 | $domain = Domain::where('domain', $host) 39 | ->where('status', Status::ACTIVE) 40 | ->first(); 41 | 42 | // If the domain is not found, we redirect to the website. 43 | if(!$domain) { 44 | return $next(config('app.website')); 45 | } 46 | 47 | // if domain provides a not found url, we redirect to that url. 48 | if($domain->not_found_url) { 49 | return Inertia::location($domain->not_found_url); 50 | } 51 | 52 | // default route 53 | return Inertia::location(config('app.website')); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Http/Middleware/HandleInertiaRequests.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | public function share(Request $request): array 33 | { 34 | return [ 35 | ...parent::share($request), 36 | 'auth' => [ 37 | 'user' => function () use ($request) { 38 | if (! $request->user()) { 39 | return; 40 | } 41 | 42 | $currentWorkspace = $request->user()->current_workspace_id ? $request->user()->currentWorkspace : null; 43 | $currentWorkspace ? $currentWorkspace->role = $request->user()->workspaceRole($currentWorkspace) : null; 44 | 45 | return array_merge($request->user()->toArray(), array_filter([ 46 | 'current_workspace' => $currentWorkspace, 47 | 'workspaces' => $request->user()->workspaces, 48 | ])); 49 | }, 50 | ], 51 | 'csrf_token' => csrf_token(), 52 | 'flash' => $request->session()->get('flash', []), 53 | 'env' => config('app.env'), 54 | 'locale' => app()->getLocale(), 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/Http/Middleware/SetWorkspace.php: -------------------------------------------------------------------------------- 1 | user()->workspaces->count() == 0 && !request()->routeIs('workspaces.*')) { 16 | return redirect(route('workspaces.create')); 17 | } 18 | return $next($request); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Requests/Account/UpdateRequest.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function rules(): array 21 | { 22 | return [ 23 | 'name' => ['string', 'max:255'], 24 | 'email' => ['email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], 25 | 'current_password' => ['nullable', 'string', 'current_password:web'], 26 | 'password' => ['nullable', 'confirmed', Rules\Password::defaults(), Rule::requiredIf($this->current_password ? true : false)], 27 | ]; 28 | } 29 | 30 | /** 31 | * Get the error messages for the defined validation rules. 32 | * 33 | * @return array 34 | */ 35 | public function messages(): array 36 | { 37 | return [ 38 | 'current_password.current_password' => __('The provided password does not match your current password.'), 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Http/Requests/Domain/CreateRequest.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'required', 20 | 'regex:/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i', // no schema 21 | Rule::unique('domains') 22 | ->whereNull('deleted_at'), 23 | Rule::notIn(config('domains.available')) 24 | ], 25 | 'not_found_url' => ['nullable', 'max:255', 'url'], 26 | 'expired_url' => ['nullable', 'max:255', 'url'], 27 | ]; 28 | } 29 | 30 | public function messages() 31 | { 32 | return [ 33 | 'domain.regex' => 'The domain format is invalid, do not include schema at the beginning of the domain.', 34 | 'domain.unique' => 'The domain has already been taken.', 35 | 'domain.not_in' => 'The domain is not allowed.', 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Requests/Domain/UpdateRequest.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'required', 20 | 'regex:/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i', // no schema 21 | Rule::unique('domains') 22 | ->ignore($this->id) 23 | ->whereNull('deleted_at'), 24 | Rule::notIn(config('domains.available')) 25 | ], 26 | 'not_found_url' => ['nullable', 'max:255', 'url'], 27 | 'expired_url' => ['nullable', 'max:255', 'url'], 28 | ]; 29 | } 30 | 31 | public function messages() 32 | { 33 | return [ 34 | 'domain.regex' => 'The domain format is invalid, do not include schema at the beginning of the domain.', 35 | 'domain.unique' => 'The domain has already been taken.', 36 | 'domain.not_in' => 'The domain is not allowed.', 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Http/Requests/Invite/InviteRequest.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | public function rules(): array 27 | { 28 | return [ 29 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:invites'], 30 | 'role' => [new Enum(Role::class)], 31 | 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Http/Requests/ProfileUpdateRequest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function rules(): array 19 | { 20 | return [ 21 | 'name' => ['required', 'string', 'max:255'], 22 | 'email' => [ 23 | 'required', 24 | 'string', 25 | 'lowercase', 26 | 'email', 27 | 'max:255', 28 | Rule::unique(User::class)->ignore($this->user()->id), 29 | ], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Requests/Tag/CreateRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'max:255', 'min:2'], 21 | 'color' => ['required','string', 'max:255', new Enum(Color::class)], 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Requests/Tag/UpdateRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'max:255', 'min:2'], 21 | 'color' => ['required', 'string', 'max:255', new Enum(Color::class)], 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Requests/TeamMember/UpdateUserRoleRequest.php: -------------------------------------------------------------------------------- 1 | [new Enum(UserRole::class)], 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Requests/Workspace/CreateRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'max:255', 'min:2'], 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Requests/Workspace/UpdateRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'max:255'], 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Resources/Api/DomainResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => $this->id, 19 | 'workspace_id' => $this->workspace_id, 20 | 'domain' => $this->domain, 21 | 'status' => $this->status, 22 | 'not_found_url' => $this->not_found_url, 23 | 'expired_url' => $this->expired_url, 24 | 'created_at' => $this->created_at->format('Y-m-d H:i:s'), 25 | 'updated_at' => $this->updated_at->format('Y-m-d H:i:s'), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Resources/Api/LinkResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => $this->id, 19 | 'workspace_id' => $this->workspace_id, 20 | 'domain' => $this->domain, 21 | 'key' => $this->key, 22 | 'url' => $this->url, 23 | 'ios' => $this->ios, 24 | 'android' => $this->android, 25 | 'link' => $this->link, 26 | 'utm_source' => $this->utm_source, 27 | 'utm_medium' => $this->utm_medium, 28 | 'utm_campaign' => $this->utm_campaign, 29 | 'utm_term' => $this->utm_term, 30 | 'utm_content' => $this->utm_content, 31 | 'utm_name' => $this->utm_name, 32 | 'clicks' => $this->clicks, 33 | 'last_click' => $this->last_click, 34 | 'external_id' => $this->external_id, 35 | 'password' => $this->password, 36 | 'expires_at' => $this->expires_at ? $this->expires_at->format('Y-m-d H:i:s') : null, 37 | 'expired_redirect_url' => $this->expired_redirect_url, 38 | 'tags' => TagResource::collection($this->tags), 39 | 'created_at' => $this->created_at->format('Y-m-d H:i:s'), 40 | 'updated_at' => $this->updated_at->format('Y-m-d H:i:s'), 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Http/Resources/Api/TagResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => $this->id, 19 | 'workspace_id' => $this->workspace_id, 20 | 'name' => $this->name, 21 | 'color' => $this->color, 22 | 'created_at' => $this->created_at->format('Y-m-d H:i:s'), 23 | 'updated_at' => $this->updated_at->format('Y-m-d H:i:s'), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Mail/Team/SendUserInvite.php: -------------------------------------------------------------------------------- 1 | onQueue('emails'); 34 | } 35 | 36 | /** 37 | * Get the message envelope. 38 | */ 39 | public function envelope(): Envelope 40 | { 41 | return new Envelope( 42 | from: new Address(config('mail.from.address'), config('mail.from.name')), 43 | subject: "You are invited to join the {$this->workspace->name} team.", 44 | ); 45 | } 46 | 47 | /** 48 | * Get the message content definition. 49 | */ 50 | public function content(): Content 51 | { 52 | return new Content( 53 | view: 'mail.invite', 54 | with: [ 55 | 'title' => "You are invited to join the {$this->workspace->name} team.", 56 | 'workspace' => $this->workspace, 57 | 'url' => route('auth.invites.show', $this->invite->id), 58 | ], 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Models/ApiToken.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected $fillable = [ 25 | 'workspace_id', 26 | 'name', 27 | 'token', 28 | 'last_used_at' 29 | ]; 30 | 31 | /** 32 | * The attributes that should be hidden for serialization. 33 | * 34 | * @var array 35 | */ 36 | protected $hidden = [ 37 | 'token' 38 | ]; 39 | 40 | /** 41 | * The attributes that should be cast. 42 | * 43 | * @var array 44 | */ 45 | protected function casts(): array 46 | { 47 | return [ 48 | 'last_used_at' => 'datetime', 49 | ]; 50 | } 51 | 52 | public function workspace(): BelongsTo 53 | { 54 | return $this->belongsTo(Workspace::class); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/Models/Domain.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | protected $fillable = [ 31 | 'workspace_id', 32 | 'domain', 33 | 'status', 34 | 'not_found_url', 35 | 'expired_url', 36 | ]; 37 | 38 | /** 39 | * The attributes that should be hidden for serialization. 40 | * 41 | * @var array 42 | */ 43 | protected $hidden = []; 44 | 45 | /** 46 | * The attributes that should be cast. 47 | * 48 | * @var array 49 | */ 50 | protected function casts(): array 51 | { 52 | return [ 53 | 'status' => Status::class, 54 | ]; 55 | } 56 | 57 | public function workspace(): BelongsTo 58 | { 59 | return $this->belongsTo(Workspace::class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Models/Invite.php: -------------------------------------------------------------------------------- 1 | belongsTo(Workspace::class); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Models/LinkStat.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected $fillable = [ 25 | 'link_id', 26 | 'workspace_id', 27 | 'event', 28 | 'country', 29 | 'region', 30 | 'city', 31 | 'os', 32 | 'device', 33 | 'browser', 34 | 'ip', 35 | 'language', 36 | 'utm_medium', 37 | 'utm_source', 38 | 'utm_campaign', 39 | 'utm_content', 40 | 'utm_term', 41 | 42 | 'referer', 43 | ]; 44 | 45 | /** 46 | * The attributes that should be hidden for serialization. 47 | * 48 | * @var array 49 | */ 50 | protected $hidden = []; 51 | 52 | /** 53 | * The attributes that should be cast. 54 | * 55 | * @var array 56 | */ 57 | protected function casts(): array 58 | { 59 | return [ 60 | 'last_click' => 'datetime', 61 | 'event' => Event::class, 62 | ]; 63 | } 64 | 65 | public function link(): BelongsTo 66 | { 67 | return $this->belongsTo(Link::class); 68 | } 69 | 70 | public function workspace(): BelongsTo 71 | { 72 | return $this->belongsTo(Workspace::class); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Models/Plan.php: -------------------------------------------------------------------------------- 1 | belongsTo(Workspace::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Models/Scopes/MediaScope.php: -------------------------------------------------------------------------------- 1 | orderBy('order_column', 'asc'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Models/Scopes/TagScope.php: -------------------------------------------------------------------------------- 1 | orderBy('sort', 'asc'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Models/Tag.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | protected function casts(): array 41 | { 42 | return [ 43 | 'color' => Color::class, 44 | ]; 45 | } 46 | 47 | protected static function booted() 48 | { 49 | static::addGlobalScope(new TagScope); 50 | } 51 | 52 | public function workspace(): BelongsTo 53 | { 54 | return $this->belongsTo(Workspace::class); 55 | } 56 | 57 | public function links(): BelongsToMany 58 | { 59 | return $this->belongsToMany(Link::class)->withTimestamps(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Observers/DomainObserver.php: -------------------------------------------------------------------------------- 1 | redis = Redis::connection('default'); 20 | } 21 | 22 | /** 23 | * Handle the Domain "created" event. 24 | */ 25 | public function created(Domain $domain): void 26 | { 27 | $this->redis->set($domain->domain, "lua.sha"); 28 | 29 | } 30 | 31 | /** 32 | * Handle the Domain "updated" event. 33 | */ 34 | public function updated(Domain $domain): void 35 | { 36 | // delete the old domain 37 | $this->redis->del($domain->getOriginal('domain')); 38 | 39 | // set the new domain 40 | $this->redis->set($domain->domain, "lua.sha"); 41 | } 42 | 43 | /** 44 | * Handle the Domain "deleted" event. 45 | */ 46 | public function deleted(Domain $domain): void 47 | { 48 | $this->redis->del($domain->domain); 49 | } 50 | 51 | /** 52 | * Handle the Domain "restored" event. 53 | */ 54 | public function restored(Domain $domain): void 55 | { 56 | // 57 | } 58 | 59 | /** 60 | * Handle the Domain "force deleted" event. 61 | */ 62 | public function forceDeleted(Domain $domain): void 63 | { 64 | // 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/Observers/WorkspaceObserver.php: -------------------------------------------------------------------------------- 1 | $workspace->id, 20 | 'sort' => 1, 21 | 'name' => 'Marketing', 22 | 'color' => 'red' 23 | ]); 24 | 25 | Tag::create([ 26 | 'workspace_id' => $workspace->id, 27 | 'sort' => 2, 28 | 'name' => 'Sales', 29 | 'color' => 'blue' 30 | ]); 31 | 32 | Tag::create([ 33 | 'workspace_id' => $workspace->id, 34 | 'sort' => 3, 35 | 'name' => 'Development', 36 | 'color' => 'green' 37 | ]); 38 | } 39 | 40 | /** 41 | * Handle the Workspace "updated" event. 42 | */ 43 | public function updated(Workspace $workspace): void 44 | { 45 | // 46 | } 47 | 48 | /** 49 | * Handle the Workspace "deleted" event. 50 | */ 51 | public function deleted(Workspace $workspace): void 52 | { 53 | // 54 | } 55 | 56 | /** 57 | * Handle the Workspace "restored" event. 58 | */ 59 | public function restored(Workspace $workspace): void 60 | { 61 | // 62 | } 63 | 64 | /** 65 | * Handle the Workspace "force deleted" event. 66 | */ 67 | public function forceDeleted(Workspace $workspace): void 68 | { 69 | // 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/Policies/WorkspacePolicy.php: -------------------------------------------------------------------------------- 1 | usage()['links']['reached_limit']; 18 | } 19 | 20 | /** 21 | * Determine if the user has reached the events limit. 22 | */ 23 | public function reachedEventLimit(?User $user, Workspace $workspace): bool 24 | { 25 | return !$workspace->usage()['events']['reached_limit']; 26 | } 27 | 28 | /** 29 | * Determine if the user has reached the domain limit. 30 | */ 31 | public function reachedDomainLimit(?User $user, Workspace $workspace): bool 32 | { 33 | return !$workspace->usage()['domains']['reached_limit']; 34 | } 35 | 36 | /** 37 | * Determine if the user has reached the tag limit. 38 | */ 39 | public function reachedTagLimit(?User $user, Workspace $workspace): bool 40 | { 41 | return !$workspace->usage()['tags']['reached_limit']; 42 | } 43 | 44 | /** 45 | * Determine if the user has reached the team member limit. 46 | */ 47 | public function reachedUserLimit(?User $user, Workspace $workspace): bool 48 | { 49 | return !$workspace->usage()['users']['reached_limit']; 50 | } 51 | 52 | 53 | 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/Providers/HorizonServiceProvider.php: -------------------------------------------------------------------------------- 1 | email, [ 30 | 'paulo@lua.sh' 31 | ]); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Providers/TelescopeServiceProvider.php: -------------------------------------------------------------------------------- 1 | hideSensitiveRequestDetails(); 22 | 23 | $isLocal = $this->app->environment('local'); 24 | 25 | Telescope::filter(function (IncomingEntry $entry) use ($isLocal) { 26 | return $isLocal || 27 | $entry->isReportableException() || 28 | $entry->isFailedRequest() || 29 | $entry->isFailedJob() || 30 | $entry->isScheduledTask() || 31 | $entry->hasMonitoredTag(); 32 | }); 33 | } 34 | 35 | /** 36 | * Prevent sensitive request details from being logged by Telescope. 37 | */ 38 | protected function hideSensitiveRequestDetails(): void 39 | { 40 | if ($this->app->environment('local')) { 41 | return; 42 | } 43 | 44 | Telescope::hideRequestParameters(['_token']); 45 | 46 | Telescope::hideRequestHeaders([ 47 | 'cookie', 48 | 'x-csrf-token', 49 | 'x-xsrf-token', 50 | ]); 51 | } 52 | 53 | /** 54 | * Register the Telescope gate. 55 | * 56 | * This gate determines who can access Telescope in non-local environments. 57 | */ 58 | protected function gate(): void 59 | { 60 | Gate::define('viewTelescope', function ($user) { 61 | return in_array($user->email, [ 62 | // 63 | ]); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/Services/MediaUrlGeneratorService.php: -------------------------------------------------------------------------------- 1 | media->visibility == Visibility::PRIVATE) { 16 | return $this->getTemporaryUrl( 17 | now()->addMinutes(30), 18 | ['ResponseContentDisposition' => 'attachment'] 19 | ); 20 | } 21 | 22 | $url = $this->getDisk()->url($this->getPathRelativeToRoot()); 23 | return $this->versionUrl($url); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | ['api/*'], 21 | 22 | 'allowed_methods' => ['*'], 23 | 24 | 'allowed_origins' => ['*'], 25 | 26 | 'allowed_origins_patterns' => [], 27 | 28 | 'allowed_headers' => ['*'], 29 | 30 | 'exposed_headers' => [], 31 | 32 | 'max_age' => 0, 33 | 34 | 'supports_credentials' => false, 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /config/domains.php: -------------------------------------------------------------------------------- 1 | env('DOMAIN_MAIN', 'lua.sh'), 13 | 14 | 'cname' => env('DOMAIN_CNAME', 'cname.lua.sh'), 15 | 16 | 'available' => [ 17 | env('DOMAIN_MAIN', 'lua.sh'), 18 | 'git.now', 19 | 'cal.now', 20 | 'fig.now', 21 | 'spoti.now' 22 | ] 23 | ]; 24 | -------------------------------------------------------------------------------- /config/pennant.php: -------------------------------------------------------------------------------- 1 | env('PENNANT_STORE', 'database'), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Pennant Stores 25 | |-------------------------------------------------------------------------- 26 | | 27 | | Here you may configure each of the stores that should be available to 28 | | Pennant. These stores shall be used to store resolved feature flag 29 | | values - you may configure as many as your application requires. 30 | | 31 | */ 32 | 33 | 'stores' => [ 34 | 35 | 'array' => [ 36 | 'driver' => 'array', 37 | ], 38 | 39 | 'database' => [ 40 | 'driver' => 'database', 41 | 'connection' => null, 42 | 'table' => 'features', 43 | ], 44 | 45 | ], 46 | ]; 47 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'token' => env('POSTMARK_TOKEN'), 21 | ], 22 | 23 | 'ses' => [ 24 | 'key' => env('AWS_ACCESS_KEY_ID'), 25 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 26 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 27 | ], 28 | 29 | 'resend' => [ 30 | 'key' => env('RESEND_KEY'), 31 | ], 32 | 33 | 'slack' => [ 34 | 'notifications' => [ 35 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 36 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 37 | ], 38 | ], 39 | 40 | ]; 41 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/ApiTokenFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ApiTokenFactory extends Factory 15 | { 16 | /** 17 | * Define the model's default state. 18 | * 19 | * @return array 20 | */ 21 | public function definition(): array 22 | { 23 | return [ 24 | 'workspace_id' => Workspace::factory(), 25 | 'name' => $this->faker->name, 26 | 'token' => $this->faker->uuid, 27 | 'last_used_at' => now(), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/factories/DomainFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class DomainFactory extends Factory 16 | { 17 | /** 18 | * Define the model's default state. 19 | * 20 | * @return array 21 | */ 22 | public function definition(): array 23 | { 24 | return [ 25 | 'workspace_id' => Workspace::factory(), 26 | 'domain' => $this->faker->domainName, 27 | 'status' => Status::ACTIVE, 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/factories/InviteFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class InviteFactory extends Factory 16 | { 17 | /** 18 | * Define the model's default state. 19 | * 20 | * @return array 21 | */ 22 | public function definition(): array 23 | { 24 | return [ 25 | 'email' => $this->faker->email, 26 | 'role' => Role::ROLE_ADMIN, 27 | 'workspace_id' => Workspace::factory(), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/factories/LinkFactory.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class LinkFactory extends Factory 17 | { 18 | /** 19 | * Define the model's default state. 20 | * 21 | * @return array 22 | */ 23 | public function definition(): array 24 | { 25 | $slug = Str::random(7); 26 | $domain = config('domains.main'); 27 | 28 | return [ 29 | 'workspace_id' => Workspace::factory(), 30 | 'domain' => $domain, 31 | 'key' => $slug, 32 | 'url' => $this->faker->url, 33 | 'link' => "https://{$domain}/{$slug}", 34 | 'ios' => $this->faker->url, 35 | 'android' => $this->faker->url, 36 | 'utm_source' => $this->faker->word, 37 | 'utm_medium' => $this->faker->word, 38 | 'utm_campaign' => $this->faker->word, 39 | 'utm_term' => $this->faker->word, 40 | 'utm_content' => $this->faker->word, 41 | 'clicks' => $this->faker->randomNumber(), 42 | 'last_click' => $this->faker->dateTime, 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /database/factories/PlanFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class PlanFactory extends Factory 14 | { 15 | /** 16 | * Define the model's default state. 17 | * 18 | * @return array 19 | */ 20 | public function definition(): array 21 | { 22 | return [ 23 | 'name' => 'Scale', 24 | 'internal_id' => Str::uuid(), 25 | 'price' => 3490, 26 | 'is_monthly' => false, 27 | 'stripe_id' => '', 28 | 'access_level' => 5, 29 | 'is_private' => false, 30 | 'max_links' => 100000, 31 | 'max_events' => 2000000, 32 | 'max_users' => 20, 33 | 'max_tags' => 1000, 34 | 'max_domains' => 500, 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/factories/TagFactory.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class TagFactory extends Factory 17 | { 18 | /** 19 | * Define the model's default state. 20 | * 21 | * @return array 22 | */ 23 | public function definition(): array 24 | { 25 | return [ 26 | 'workspace_id' => Workspace::factory(), 27 | 'name' => $this->faker->word, 28 | 'color' => Color::GREEN, 29 | 'sort' => $this->faker->randomNumber(), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class UserFactory extends Factory 21 | { 22 | /** 23 | * The current password being used by the factory. 24 | */ 25 | protected static ?string $password; 26 | 27 | /** 28 | * Define the model's default state. 29 | * 30 | * @return array 31 | */ 32 | public function definition(): array 33 | { 34 | return [ 35 | 'name' => fake()->name(), 36 | 'email' => fake()->unique()->safeEmail(), 37 | 'email_verified_at' => now(), 38 | 'password' => static::$password ??= Hash::make('password'), 39 | 'remember_token' => Str::random(10), 40 | 'theme' => Theme::SYSTEM, 41 | ]; 42 | } 43 | 44 | /** 45 | * Indicate that the model's email address should be unverified. 46 | */ 47 | public function unverified(): static 48 | { 49 | return $this->state(fn (array $attributes) => [ 50 | 'email_verified_at' => null, 51 | ]); 52 | } 53 | 54 | public function withWorkspace() 55 | { 56 | return $this->afterCreating(function (User $user) { 57 | 58 | $workspace = Workspace::factory()->create(); 59 | $user->workspaces()->attach($workspace->id, ['role' => Role::ROLE_OWNER]); 60 | 61 | // set the current team 62 | $user->current_workspace_id = $workspace->id; 63 | $user->save(); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /database/factories/WorkspaceFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class WorkspaceFactory extends Factory 15 | { 16 | /** 17 | * Define the model's default state. 18 | * 19 | * @return array 20 | */ 21 | public function definition(): array 22 | { 23 | return [ 24 | 'name' => $this->faker->name(), 25 | 'plan_id' => Plan::factory(), 26 | 'billing_cycle_start' => now()->day, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/factories/data/languages.json: -------------------------------------------------------------------------------- 1 | [ 2 | "en", 3 | "zh", 4 | "de", 5 | "ru", 6 | "fr", 7 | "pt", 8 | "pl", 9 | "ja", 10 | "it", 11 | "es", 12 | "uk", 13 | "id", 14 | "sv", 15 | "ko", 16 | "nb", 17 | "ca", 18 | "sk", 19 | "tr", 20 | "vi", 21 | "el", 22 | "hu", 23 | "nl", 24 | "nn" 25 | ] 26 | -------------------------------------------------------------------------------- /database/factories/data/userAgent.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Mozilla/5.0 (Macintosh; PPC Mac OS X 10_8_1) AppleWebKit/5352 (KHTML, like Gecko) Chrome/40.0.848.0 Mobile Safari/5352", 3 | "Mozilla/5.0 (X11; Linux i686; rv:7.0) Gecko/20121220 Firefox/35.0", 4 | "Mozilla/5.0 (Macintosh; PPC Mac OS X 10_8_3 rv:5.0; sl-SI) AppleWebKit/532.33.2 (KHTML, like Gecko) Version/5.0 Safari/532.33.2", 5 | "Opera/8.55 (Windows 95; en-US) Presto/2.9.286 Version/11.00", 6 | "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 5.0; Trident/5.1)", 7 | "Mozilla/5.0 (Linux; Android 11; SM-G9910) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.181 Mobile Safari/537.36", 8 | "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.111 Mobile Safari/537.36", 9 | "Mozilla/5.0 (iPhone; CPU iPhone OS 14_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/92.0.4515.90 Mobile/15E148 Safari/604.1", 10 | "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Mobile/15E148 Safari/604.1", 11 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Safari/605.1.15", 12 | "Mozilla/5.0 (Linux; Android 5.0.2; SAMSUNG SM-T550 Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/3.3 Chrome/38.0.2125.102 Safari/537.36", 13 | "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/11.10240" 14 | ] 15 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->timestamp('email_verified_at')->nullable(); 21 | $table->string('password')->nullable(); 22 | $table->string('photo')->nullable(); 23 | $table->foreignUuid('current_workspace_id')->nullable(); 24 | $table->string('theme')->default(Theme::SYSTEM); 25 | $table->rememberToken(); 26 | $table->timestamps(); 27 | }); 28 | 29 | Schema::create('password_reset_tokens', function (Blueprint $table) { 30 | $table->string('email')->primary(); 31 | $table->string('token'); 32 | $table->timestamp('created_at')->nullable(); 33 | }); 34 | 35 | Schema::create('sessions', function (Blueprint $table) { 36 | $table->string('id')->primary(); 37 | $table->foreignUuid('user_id')->nullable()->index(); 38 | $table->string('ip_address', 45)->nullable(); 39 | $table->text('user_agent')->nullable(); 40 | $table->longText('payload'); 41 | $table->integer('last_activity')->index(); 42 | }); 43 | } 44 | 45 | /** 46 | * Reverse the migrations. 47 | */ 48 | public function down(): void 49 | { 50 | Schema::dropIfExists('users'); 51 | Schema::dropIfExists('password_reset_tokens'); 52 | Schema::dropIfExists('sessions'); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2023_05_05_001123_create_plans_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 16 | $table->string('name'); 17 | $table->string('internal_id')->unique(); 18 | $table->integer('price'); 19 | $table->boolean('is_monthly'); 20 | $table->string('stripe_id')->nullable(); 21 | $table->integer('access_level'); 22 | $table->boolean('is_private'); 23 | $table->integer('max_users')->nullable(); 24 | $table->integer('max_tags')->nullable(); 25 | $table->integer('max_domains')->nullable(); 26 | $table->integer('max_links'); 27 | $table->integer('max_events'); 28 | $table->timestamps(); 29 | $table->softDeletes(); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | */ 36 | public function down(): void 37 | { 38 | Schema::dropIfExists('plans'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /database/migrations/2023_05_05_001124_create_workspaces_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 18 | $table->string('name'); 19 | $table->string('logo')->nullable(); 20 | $table->foreignUuid('plan_id')->constrained(); 21 | 22 | $table->string('stripe_id')->nullable()->index(); 23 | $table->string('pm_type')->nullable(); 24 | $table->string('pm_last_four', 4)->nullable(); 25 | $table->timestamp('trial_ends_at')->nullable(); 26 | $table->integer('billing_cycle_start')->nullable(); 27 | $table->timestamps(); 28 | $table->softDeletes(); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | */ 35 | public function down(): void 36 | { 37 | Schema::dropIfExists('workspaces'); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /database/migrations/2023_05_05_001144_create_user_workspace_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignUuid('workspace_id')->constrained(); 19 | $table->foreignUuid('user_id')->constrained(); 20 | $table->string('role'); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('user_workspace'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2023_05_06_230755_create_invites_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 20 | $table->string('email'); 21 | $table->string('role'); 22 | $table->foreignUuid('workspace_id')->constrained(); 23 | 24 | $table->timestamps(); 25 | $table->unique(['email', 'workspace_id']); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('invites'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /database/migrations/2024_10_18_031325_create_domains_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 16 | $table->foreignUuid('workspace_id')->constrained(); 17 | $table->string('domain'); 18 | $table->string('status'); 19 | $table->string('not_found_url')->nullable(); 20 | $table->string('expired_url')->nullable(); 21 | $table->timestamps(); 22 | $table->softDeletes(); 23 | 24 | $table->unique(['domain', 'deleted_at']); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('domains'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2024_10_18_031326_create_links_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 16 | $table->foreignUuid('workspace_id')->constrained(); 17 | 18 | $table->string('domain'); // domain of the link (e.g. lua.sh) 19 | $table->string('key'); // key of the link (e.g. /github) 20 | $table->string('url'); // target url (e.g. https://github.com/luadotsh) 21 | $table->string('link', 600)->unique(); // full link (e.g. https://lua.sh/github) 22 | 23 | // utm parameters 24 | $table->string('utm_source')->nullable(); 25 | $table->string('utm_medium')->nullable(); 26 | $table->string('utm_campaign')->nullable(); 27 | $table->string('utm_term')->nullable(); 28 | $table->string('utm_content')->nullable(); 29 | $table->string('utm_name')->nullable(); 30 | 31 | $table->unsignedBigInteger('clicks')->default(0); 32 | $table->dateTime('last_click')->nullable(); 33 | 34 | $table->string('external_id')->nullable(); 35 | $table->string('password')->nullable(); 36 | $table->timestamps(); 37 | 38 | $table->unique(['domain', 'key']); 39 | $table->unique(['workspace_id', 'external_id']); 40 | }); 41 | } 42 | 43 | /** 44 | * Reverse the migrations. 45 | */ 46 | public function down(): void 47 | { 48 | Schema::dropIfExists('links'); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /database/migrations/2024_10_18_032933_create_subscriptions_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignUuid('workspace_id'); 17 | $table->string('type'); 18 | $table->string('stripe_id')->unique(); 19 | $table->string('stripe_status'); 20 | $table->string('stripe_price')->nullable(); 21 | $table->integer('quantity')->nullable(); 22 | $table->timestamp('trial_ends_at')->nullable(); 23 | $table->timestamp('ends_at')->nullable(); 24 | $table->timestamps(); 25 | 26 | $table->index(['workspace_id', 'stripe_status']); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | */ 33 | public function down(): void 34 | { 35 | Schema::dropIfExists('subscriptions'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/2024_10_18_032934_create_subscription_items_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('subscription_id'); 17 | $table->string('stripe_id')->unique(); 18 | $table->string('stripe_product'); 19 | $table->string('stripe_price'); 20 | $table->integer('quantity')->nullable(); 21 | $table->timestamps(); 22 | 23 | $table->index(['subscription_id', 'stripe_price']); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('subscription_items'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2024_10_18_034126_create_link_stats_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 16 | $table->foreignUuid('workspace_id')->constrained(); 17 | $table->foreignUuid('link_id')->constrained()->onDelete('cascade'); 18 | 19 | $table->string('event')->nullable(); 20 | 21 | $table->string('country')->nullable(); 22 | $table->string('region')->nullable(); 23 | $table->string('city')->nullable(); 24 | 25 | $table->string('os')->nullable(); 26 | $table->string('device')->nullable(); 27 | $table->string('browser')->nullable(); 28 | $table->string('language')->nullable(); 29 | 30 | $table->ipAddress('ip')->nullable(); 31 | 32 | $table->string('utm_medium')->nullable(); 33 | $table->string('utm_source')->nullable(); 34 | $table->string('utm_campaign')->nullable(); 35 | $table->string('utm_content')->nullable(); 36 | $table->string('utm_term')->nullable(); 37 | 38 | $table->string('referer', 900)->nullable(); 39 | 40 | $table->timestamps(); 41 | }); 42 | } 43 | 44 | /** 45 | * Reverse the migrations. 46 | */ 47 | public function down(): void 48 | { 49 | Schema::dropIfExists('link_stats'); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /database/migrations/2024_10_18_210819_create_medias_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 15 | $table->uuidMorphs('model'); 16 | $table->uuid()->nullable()->unique(); 17 | $table->string('collection_name'); 18 | $table->string('name'); 19 | $table->string('file_name'); 20 | $table->string('mime_type')->nullable(); 21 | $table->string('disk'); 22 | $table->string('visibility'); 23 | $table->string('conversions_disk')->nullable(); 24 | $table->unsignedBigInteger('size'); 25 | $table->json('manipulations'); 26 | $table->json('custom_properties'); 27 | $table->json('generated_conversions'); 28 | $table->json('responsive_images'); 29 | $table->unsignedInteger('order_column')->nullable()->index(); 30 | $table->nullableTimestamps(); 31 | $table->softDeletes(); 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2024_10_19_170336_create_tags_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 16 | $table->foreignUuid('workspace_id')->constrained(); 17 | $table->string('name'); 18 | $table->string('color'); 19 | $table->integer('sort')->default(0); 20 | $table->timestamps(); 21 | $table->softDeletes(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('tags'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2024_10_19_183425_create_link_tag_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignUuid('link_id')->constrained()->onDelete('cascade'); 17 | $table->foreignUuid('tag_id')->constrained()->onDelete('cascade'); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('link_tag'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/2024_10_19_191838_create_api_tokens_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 16 | $table->foreignUuid('workspace_id')->constrained(); 17 | $table->string('name'); 18 | $table->uuid('token', 100)->unique(); 19 | $table->dateTime('last_used_at')->nullable(); 20 | $table->timestamps(); 21 | $table->softDeletes(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('api_tokens'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2024_12_02_215230_add_ios_and_android_to_links_table.php: -------------------------------------------------------------------------------- 1 | string('ios')->after('link')->nullable(); 16 | $table->string('android')->after('ios')->nullable(); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::table('links', function (Blueprint $table) { 26 | $table->dropColumn('ios'); 27 | $table->dropColumn('android'); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2025_01_26_165050_add_expires_at_to_links_table.php: -------------------------------------------------------------------------------- 1 | dateTime('expires_at')->nullable()->after('password'); 16 | $table->string('expired_redirect_url')->nullable()->after('expires_at'); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::table('links', function (Blueprint $table) { 26 | $table->dropColumn('expires_at'); 27 | $table->dropColumn('expired_redirect_url'); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call([ 16 | PlanSeeder::class, 17 | ]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /database/seeders/LocalDevelopmentSeeder.php: -------------------------------------------------------------------------------- 1 | 'Admin', 29 | 'email' => 'admin@lua.sh' 30 | ]) 31 | ->hasAttached( 32 | Workspace::factory([ 33 | 'plan_id' => Plan::where('internal_id', 'free')->first()->id 34 | ]), 35 | ['role' => Role::ROLE_ADMIN] 36 | ) 37 | ->create(); 38 | 39 | $workspace = Workspace::first(); 40 | 41 | // set current workspace 42 | $user->current_workspace_id = $workspace->id; 43 | $user->save(); 44 | 45 | // create some links 46 | $links = Link::factory() 47 | ->count(100) 48 | ->create([ 49 | 'workspace_id' => $workspace->id 50 | ]); 51 | 52 | $dates = CarbonPeriod::create(now()->subMonths(4), '60 minutes', now()); 53 | 54 | foreach ($dates as $date) { 55 | 56 | $link = $links->random(); 57 | 58 | // create some stats 59 | LinkStat::factory([ 60 | 'link_id' => $link->id, 61 | 'workspace_id' => $workspace->id, 62 | 'created_at' => $date 63 | ]) 64 | ->count(1) 65 | ->create(); 66 | 67 | // update the link 68 | $link->clicks = LinkStat::where('link_id', $link->id)->count(); 69 | $link->save(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["resources/js/*"], 6 | "ziggy-js": ["./vendor/tightenco/ziggy"] 7 | } 8 | }, 9 | "exclude": ["node_modules", "public"] 10 | } 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | 'Previous', 19 | 'next' => 'Next', 20 | 'showing' => 'Showing', 21 | 'to' => 'to', 22 | 'of' => 'of', 23 | 'results' => 'results', 24 | 25 | ]; 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 | -------------------------------------------------------------------------------- /maizzle/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /maizzle/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: maizzle 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /maizzle/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /maizzle/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build_local 3 | .vscode 4 | .idea 5 | Thumbs.db 6 | .DS_Store 7 | npm-debug.log 8 | yarn-error.log 9 | -------------------------------------------------------------------------------- /maizzle/.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 18.20.4 2 | -------------------------------------------------------------------------------- /maizzle/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Cosmin Popovici 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /maizzle/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 5 | 6 | Maizzle Starter 7 | 8 | 9 |

10 |

Quickly build HTML emails with Tailwind CSS

11 |
12 | 13 | [![Version][npm-version-shield]][npm] 14 | [![Build][github-ci-shield]][github-ci] 15 | [![Downloads][npm-stats-shield]][npm-stats] 16 | [![License][license-shield]][license] 17 | 18 |
19 |
20 | 21 | ## Getting Started 22 | 23 | Run this command and follow the prompts: 24 | 25 | ```bash 26 | npx create-maizzle 27 | ``` 28 | 29 | ## Documentation 30 | 31 | Maizzle documentation is available at https://maizzle.com 32 | 33 | ## Issues 34 | 35 | Please open all issues in the [framework repository](https://github.com/maizzle/framework). 36 | 37 | ## License 38 | 39 | The Maizzle framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). 40 | 41 | [npm]: https://www.npmjs.com/package/@maizzle/framework 42 | [npm-stats]: https://npm-stat.com/charts.html?package=%40maizzle%2Fframework&from=2019-03-27 43 | [npm-version-shield]: https://img.shields.io/npm/v/@maizzle/framework.svg 44 | [npm-stats-shield]: https://img.shields.io/npm/dt/@maizzle/framework.svg?color=6875f5 45 | [github-ci]: https://github.com/maizzle/framework/actions 46 | [github-ci-shield]: https://github.com/maizzle/framework/actions/workflows/nodejs.yml/badge.svg 47 | [license]: ./LICENSE 48 | [license-shield]: https://img.shields.io/npm/l/@maizzle/framework.svg?color=0e9f6e 49 | -------------------------------------------------------------------------------- /maizzle/config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@maizzle/framework').Config} */ 2 | 3 | /* 4 | |------------------------------------------------------------------------------- 5 | | Development config https://maizzle.com/docs/environments 6 | |------------------------------------------------------------------------------- 7 | | 8 | | The exported object contains the default Maizzle settings for development. 9 | | This is used when you run `maizzle build` or `maizzle serve` and it has 10 | | the fastest build time, since most transformations are disabled. 11 | | 12 | */ 13 | 14 | module.exports = { 15 | build: { 16 | posthtml: { 17 | expressions: { 18 | unescapeDelimiters: ["{!!", "!!}"], 19 | }, 20 | }, 21 | templates: { 22 | source: "src/templates", 23 | destination: { 24 | path: "build_local", 25 | }, 26 | assets: { 27 | source: "srcimages", 28 | destination: "images", 29 | }, 30 | }, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /maizzle/config.production.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@maizzle/framework').Config} */ 2 | 3 | /* 4 | |------------------------------------------------------------------------------- 5 | | Production config https://maizzle.com/docs/environments 6 | |------------------------------------------------------------------------------- 7 | | 8 | | This is where you define settings that optimize your emails for production. 9 | | These will be merged on top of the base config.js, so you only need to 10 | | specify the options that are changing. 11 | | 12 | */ 13 | 14 | module.exports = { 15 | build: { 16 | posthtml: { 17 | expressions: { 18 | unescapeDelimiters: ["{!!", "!!}"], 19 | }, 20 | }, 21 | templates: { 22 | destination: { 23 | path: "../resources/views/mail", 24 | extension: "blade.php", 25 | }, 26 | assets: { 27 | destination: "../../../publicimages/emails", 28 | }, 29 | }, 30 | }, 31 | inlineCSS: true, 32 | removeUnusedCSS: true, 33 | shorthandCSS: true, 34 | prettify: true, 35 | }; 36 | -------------------------------------------------------------------------------- /maizzle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "maizzle serve", 5 | "build": "maizzle build production" 6 | }, 7 | "dependencies": { 8 | "@maizzle/framework": "latest", 9 | "tailwindcss-preset-email": "^1.1.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /maizzle/src/components/button.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | -------------------------------------------------------------------------------- /maizzle/src/components/divider.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 31 | -------------------------------------------------------------------------------- /maizzle/src/components/footer.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 |

10 | © 2024 Lua.sh LLC. 11 |

12 | 13 |

14 | 651 N Broad St, Suite 201, Middletown, DE 19709, USA 15 |

16 | 17 |

18 | Github 21 | • 22 | Twitter 25 |

26 | 27 | @if(isset($unsubscribe_url)) 28 |

29 | 30 | Unsubscribe 31 | 32 |

33 | @endif 34 | 35 | 36 | -------------------------------------------------------------------------------- /maizzle/src/components/header.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Lua.sh 4 | 5 |
6 | -------------------------------------------------------------------------------- /maizzle/src/components/spacer.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 |
17 |
18 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /maizzle/src/components/v-fill.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /maizzle/src/components/v-image.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /maizzle/src/css/resets.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Here is where you can add your global email CSS resets. 3 | * 4 | * We use a custom, email-specific CSS reset, instead 5 | * of Tailwind's web-optimized `base` layer. 6 | * 7 | * Styles defined here will be inlined. 8 | */ 9 | 10 | img { 11 | @apply max-w-full align-middle; 12 | } 13 | -------------------------------------------------------------------------------- /maizzle/src/css/tailwind.css: -------------------------------------------------------------------------------- 1 | /* Your custom CSS resets for email */ 2 | @import "resets"; 3 | 4 | /* Tailwind CSS components */ 5 | @import "tailwindcss/components"; 6 | 7 | /** 8 | * @import here any custom CSS components - that is, CSS that 9 | * you'd want loaded before the Tailwind utilities, so the 10 | * utilities can still override them. 11 | */ 12 | 13 | /* Tailwind CSS utility classes */ 14 | @import "tailwindcss/utilities"; 15 | 16 | /* Your custom utility classes */ 17 | @import "utilities"; 18 | -------------------------------------------------------------------------------- /maizzle/src/css/utilities.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Here is where you can define your custom utility classes. 3 | * 4 | * We wrap them in the `utilities` @layer directive, so 5 | * that Tailwind moves them to the correct location. 6 | * 7 | * More info: 8 | * https://tailwindcss.com/docs/functions-and-directives#layer 9 | */ 10 | 11 | @layer utilities { 12 | .break-word { 13 | word-break: break-word; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /maizzle/src/images/maizzle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luadotsh/lua/ef7ab628ffd1a4184a84cb51bc08e412a692588d/maizzle/src/images/maizzle.png -------------------------------------------------------------------------------- /maizzle/src/layouts/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 28 | @{{ $title }} 29 | 32 | 33 | 34 | 35 |
36 | 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /maizzle/src/templates/email-verification.html: -------------------------------------------------------------------------------- 1 | 2 | 5 |
6 | 7 | 8 | 44 | 45 |
9 | 10 | 11 | 12 | 40 | 41 | 42 |
13 |

14 | Hi @{{$user->name}}, 15 |

16 | 17 |

18 | Please confirm your email address by clicking the button below: 19 |

20 | 21 | 22 | 23 |
24 | 25 | Confirm Email Address → 26 | 27 |
28 | 29 | 30 | 31 |

32 | If you didn’t create this account, you can safely ignore this email. 33 |

34 |

35 | Best regards, 36 |
37 | The Lua.sh Team 38 |

39 |
43 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /maizzle/src/templates/invite.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 34 | 35 |
6 | 7 | 8 | 9 | 10 | 30 | 31 | 32 |
11 |

12 | Hello 👋 13 |

14 | 15 |

16 | You have been invited to join the @{{ $workspace->name }} team. 17 |
18 |
19 | To accept the invitation and be part of this team, please click on the button below. 20 |

21 | 22 | 23 | 24 |
25 | 26 | Accept invite → 27 | 28 |
29 |
33 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /maizzle/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | presets: [ 4 | require('tailwindcss-preset-email'), 5 | ], 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "vite build", 6 | "dev": "vite", 7 | "prepare": "husky" 8 | }, 9 | "devDependencies": { 10 | "@commitlint/cli": "^19.5.0", 11 | "@commitlint/config-conventional": "^19.5.0", 12 | "@inertiajs/vue3": "^2.0.2", 13 | "@tailwindcss/forms": "^0.5.3", 14 | "@vitejs/plugin-vue": "^5.0.0", 15 | "all-contributors-cli": "^6.26.1", 16 | "autoprefixer": "^10.4.12", 17 | "axios": "^1.7.4", 18 | "concurrently": "^9.0.1", 19 | "husky": "^9.1.6", 20 | "laravel-echo": "^2.0.2", 21 | "laravel-vite-plugin": "^1.0", 22 | "postcss": "^8.4.31", 23 | "pusher-js": "^8.4.0-rc2", 24 | "tailwindcss": "^4.0.8", 25 | "vite": "^6.0", 26 | "vue": "^3.4.0" 27 | }, 28 | "dependencies": { 29 | "@headlessui/vue": "^1.7.23", 30 | "@kurkle/color": "^0.3.2", 31 | "@phosphor-icons/vue": "^2.2.1", 32 | "@tailwindcss/aspect-ratio": "^0.4.2", 33 | "@tailwindcss/typography": "^0.5.15", 34 | "chart.js": "^4.4.7", 35 | "dayjs": "^1.11.13", 36 | "floating-vue": "^5.2.2", 37 | "laravel-vue-i18n": "^2.7.7", 38 | "qrcode": "^1.5.4", 39 | "v-calendar": "^3.1.2", 40 | "vue3-colorpicker": "^2.3.0", 41 | "vuedraggable": "^4.1.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luadotsh/lua/ef7ab628ffd1a4184a84cb51bc08e412a692588d/public/.DS_Store -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luadotsh/lua/ef7ab628ffd1a4184a84cb51bc08e412a692588d/public/favicon.ico -------------------------------------------------------------------------------- /public/images/links/qr-base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luadotsh/lua/ef7ab628ffd1a4184a84cb51bc08e412a692588d/public/images/links/qr-base.png -------------------------------------------------------------------------------- /public/images/user/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luadotsh/lua/ef7ab628ffd1a4184a84cb51bc08e412a692588d/public/images/user/avatar.jpg -------------------------------------------------------------------------------- /public/images/websites/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luadotsh/lua/ef7ab628ffd1a4184a84cb51bc08e412a692588d/public/images/websites/favicon.png -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/vendor/telescope/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luadotsh/lua/ef7ab628ffd1a4184a84cb51bc08e412a692588d/public/vendor/telescope/favicon.ico -------------------------------------------------------------------------------- /public/vendor/telescope/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=99f84d421ae083196e0a45c3c310168b", 3 | "/app-dark.css": "/app-dark.css?id=1ea407db56c5163ae29311f1f38eb7b9", 4 | "/app.css": "/app.css?id=de4c978567bfd90b38d186937dee5ccf" 5 | } 6 | -------------------------------------------------------------------------------- /resources/css/floating.css: -------------------------------------------------------------------------------- 1 | .v-popper__popper { 2 | z-index: 10000; 3 | top: 0; 4 | left: 0; 5 | outline: none; 6 | } 7 | 8 | .v-popper__arrow-container { 9 | @apply hidden; 10 | } 11 | 12 | .v-popper--theme-tooltip .v-popper__inner { 13 | @apply text-center max-w-[280px] px-6 py-3 rounded-lg text-[13px] bg-white border border-zinc-300 dark:bg-zinc-800 dark:border-zinc-700 text-zinc-800 dark:text-zinc-300 shadow; 14 | } 15 | -------------------------------------------------------------------------------- /resources/js/Components/ActionSection.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | -------------------------------------------------------------------------------- /resources/js/Components/Announcement.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /resources/js/Components/ApplicationLogo.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/js/Components/Button.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /resources/js/Components/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 45 | -------------------------------------------------------------------------------- /resources/js/Components/ConfirmDeleteModal.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 81 | -------------------------------------------------------------------------------- /resources/js/Components/ConfirmationModal.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 44 | -------------------------------------------------------------------------------- /resources/js/Components/DialogModal.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 58 | -------------------------------------------------------------------------------- /resources/js/Components/DomainStatus.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 31 | -------------------------------------------------------------------------------- /resources/js/Components/FormSection.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 43 | -------------------------------------------------------------------------------- /resources/js/Components/Input.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 74 | -------------------------------------------------------------------------------- /resources/js/Components/InputError.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /resources/js/Components/InputHelp.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /resources/js/Components/Label.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 37 | -------------------------------------------------------------------------------- /resources/js/Components/Radio.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 45 | -------------------------------------------------------------------------------- /resources/js/Components/Sandbox.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | -------------------------------------------------------------------------------- /resources/js/Components/SectionBorder.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/js/Components/SectionTitle.vue: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /resources/js/Components/Select.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 43 | -------------------------------------------------------------------------------- /resources/js/Components/Tag.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /resources/js/Components/Textarea.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 |