├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitignore ├── .phpstorm.meta.php ├── LICENSE ├── README.md ├── _ide_helper.php ├── _ide_helper_models.php ├── api.dockerfile ├── app ├── Console │ ├── Commands │ │ ├── DeleteTempUploadedFiles.php │ │ └── DeleteUnverifiedDomains.php │ └── Kernel.php ├── Enums │ ├── AuthProviderEnum.php │ ├── ProjectTypeEnum.php │ ├── ReportTypeEnum.php │ └── StatusEnum.php ├── Exceptions │ └── Handler.php ├── Filament │ ├── Custom │ │ ├── AutoRelationManager.php │ │ └── CustomResource.php │ ├── Forms │ │ ├── Components │ │ │ ├── FileInput.php │ │ │ └── ImageInput.php │ │ └── Layouts │ │ │ ├── BasicForm.php │ │ │ ├── BasicSection.php │ │ │ ├── ComplexForm.php │ │ │ ├── ImagesSection.php │ │ │ ├── MainGroup.php │ │ │ ├── SideGroup.php │ │ │ ├── StatusSection.php │ │ │ └── TimestampsSection.php │ ├── Resources │ │ ├── CategoryResource.php │ │ ├── CategoryResource │ │ │ ├── Pages │ │ │ │ ├── CreateCategory.php │ │ │ │ ├── EditCategory.php │ │ │ │ └── ListCategories.php │ │ │ └── RelationManagers │ │ │ │ └── ProjectsRelationManager.php │ │ ├── ChangeProposalResource.php │ │ ├── ChangeProposalResource │ │ │ └── Pages │ │ │ │ ├── CreateChangeProposal.php │ │ │ │ ├── EditChangeProposal.php │ │ │ │ └── ListChangeProposals.php │ │ ├── CollectionResource.php │ │ ├── CollectionResource │ │ │ ├── Pages │ │ │ │ ├── CreateCollection.php │ │ │ │ ├── EditCollection.php │ │ │ │ └── ListCollections.php │ │ │ └── RelationManagers │ │ │ │ └── ProjectsRelationManager.php │ │ ├── DomainResource.php │ │ ├── DomainResource │ │ │ └── Pages │ │ │ │ ├── CreateDomain.php │ │ │ │ ├── EditDomain.php │ │ │ │ └── ListDomains.php │ │ ├── ProjectResource.php │ │ ├── ProjectResource │ │ │ ├── Pages │ │ │ │ ├── CreateProject.php │ │ │ │ ├── EditProject.php │ │ │ │ └── ListProjects.php │ │ │ └── RelationManagers │ │ │ │ ├── ChangeProposalsRelationManager.php │ │ │ │ ├── CollectionsRelationManager.php │ │ │ │ ├── ReleasesRelationManager.php │ │ │ │ ├── ReportsRelationManager.php │ │ │ │ └── ReviewsRelationManager.php │ │ ├── ReleaseResource.php │ │ ├── ReleaseResource │ │ │ └── Pages │ │ │ │ ├── CreateRelease.php │ │ │ │ ├── EditRelease.php │ │ │ │ └── ListReleases.php │ │ ├── ReportResource.php │ │ ├── ReportResource │ │ │ └── Pages │ │ │ │ ├── CreateReport.php │ │ │ │ ├── EditReport.php │ │ │ │ └── ListReports.php │ │ ├── ReviewResource.php │ │ ├── ReviewResource │ │ │ ├── Pages │ │ │ │ ├── CreateReview.php │ │ │ │ ├── EditReview.php │ │ │ │ └── ListReviews.php │ │ │ └── RelationManagers │ │ │ │ └── ReportsRelationManager.php │ │ ├── UserResource.php │ │ └── UserResource │ │ │ ├── Pages │ │ │ ├── CreateUser.php │ │ │ ├── EditUser.php │ │ │ └── ListUsers.php │ │ │ └── RelationManagers │ │ │ ├── CollectionsRelationManager.php │ │ │ ├── DomainsRelationManager.php │ │ │ ├── MaintainingRelationManager.php │ │ │ ├── ProjectsRelationManager.php │ │ │ ├── ReleasesRelationManager.php │ │ │ └── ReviewsRelationManager.php │ └── Tables │ │ └── Components │ │ └── TimestampsColumn.php ├── Http │ ├── Controllers │ │ ├── Auth │ │ │ ├── AuthController.php │ │ │ └── GithubController.php │ │ ├── CategoryController.php │ │ ├── CheckUpdateController.php │ │ ├── CollectionController.php │ │ ├── Controller.php │ │ ├── Domain │ │ │ ├── DomainController.php │ │ │ └── DomainVerifyController.php │ │ ├── FileUploadController.php │ │ ├── HomeController.php │ │ ├── LoginAsController.php │ │ ├── Project │ │ │ ├── ProjectController.php │ │ │ ├── ProjectImageController.php │ │ │ ├── ProjectReportController.php │ │ │ └── ScreenshotController.php │ │ ├── ReleaseController.php │ │ ├── Review │ │ │ ├── ReviewController.php │ │ │ └── ReviewReportController.php │ │ └── UserController.php │ ├── Kernel.php │ ├── Middleware │ │ ├── Authenticate.php │ │ ├── EncryptCookies.php │ │ ├── EnsureEmailIsVerified.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── TrimStrings.php │ │ ├── TrustHosts.php │ │ ├── TrustProxies.php │ │ ├── ValidateSignature.php │ │ └── VerifyCsrfToken.php │ ├── Requests │ │ ├── Project │ │ │ ├── StoreProjectRequest.php │ │ │ └── UpdateProjectRequest.php │ │ ├── Release │ │ │ └── StoreReleaseRequest.php │ │ ├── ReviewRequest.php │ │ └── StoreReportRequest.php │ └── Resources │ │ ├── CategoryResource.php │ │ ├── ChangeProposalResource.php │ │ ├── CheckUpdateResource.php │ │ ├── CollectionResource.php │ │ ├── DomainResource.php │ │ ├── Media │ │ └── ImageResource.php │ │ ├── Project │ │ ├── ProjectFullResource.php │ │ ├── ProjectResource.php │ │ └── ProjectSlimResource.php │ │ ├── Release │ │ ├── ReleaseFullResource.php │ │ └── ReleaseResource.php │ │ ├── ReviewResource.php │ │ └── User │ │ ├── AuthResource.php │ │ └── UserResource.php ├── Models │ ├── Category.php │ ├── ChangeProposal.php │ ├── Collection.php │ ├── Domain.php │ ├── Maintainer.php │ ├── Media.php │ ├── Project.php │ ├── Release.php │ ├── Report.php │ ├── Review.php │ ├── Scopes │ │ └── ActiveScope.php │ └── User.php ├── Observers │ └── UserObserver.php ├── Policies │ ├── DomainPolicy.php │ ├── ProjectPolicy.php │ ├── ReleasePolicy.php │ └── ReviewPolicy.php ├── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── BroadcastServiceProvider.php │ ├── EventServiceProvider.php │ ├── Filament │ │ └── AdminPanelProvider.php │ ├── HorizonServiceProvider.php │ ├── RouteServiceProvider.php │ └── TelescopeServiceProvider.php ├── Rules │ ├── FileUpload.php │ └── Username.php ├── Services │ ├── CategoryService.php │ ├── DomainService.php │ ├── FilesystemService.php │ ├── ProjectService.php │ ├── ReleaseService.php │ └── UserService.php └── Validations │ ├── ValidateReleaseFile.php │ └── ValidateReleaseVersionName.php ├── artisan ├── bootstrap ├── app.php └── cache │ └── .gitignore ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── broadcasting.php ├── cache.php ├── cors.php ├── database.php ├── filesystems.php ├── hashing.php ├── horizon.php ├── logging.php ├── mail.php ├── media-library.php ├── octane.php ├── query-builder.php ├── queue.php ├── sanctum.php ├── scramble.php ├── services.php ├── session.php ├── settings.php ├── telescope.php └── view.php ├── database ├── .gitignore ├── factories │ ├── CategoryFactory.php │ ├── ChangeProposalFactory.php │ ├── CollectionFactory.php │ ├── DomainFactory.php │ ├── MaintainerFactory.php │ ├── MediaFactory.php │ ├── ProjectFactory.php │ ├── ReleaseFactory.php │ ├── ReportFactory.php │ ├── ReviewFactory.php │ └── UserFactory.php ├── migrations │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2014_10_12_100000_create_password_reset_tokens_table.php │ ├── 2018_08_08_100000_create_telescope_entries_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ ├── 2019_12_14_000002_create_sessions_table.php │ ├── 2022_12_14_083707_create_settings_table.php │ ├── 2023_11_19_205100_create_categories_table.php │ ├── 2023_11_19_205104_create_projects_table.php │ ├── 2023_11_20_061313_create_media_table.php │ ├── 2023_11_20_125316_create_maintainers_table.php │ ├── 2023_11_23_125556_create_collections_table.php │ ├── 2023_11_23_132320_create_collection_project_table.php │ ├── 2023_11_23_133825_create_releases_table.php │ ├── 2023_11_24_124806_create_reviews_table.php │ ├── 2023_11_29_115411_create_reports_table.php │ ├── 2024_05_25_073616_create_domains_table.php │ ├── 2024_06_26_200348_create_change_proposals_table.php │ └── 2024_10_14_193910_add_reviewer_description_to_projects_table.php └── seeders │ ├── CollectionSeeder.php │ ├── DatabaseSeeder.php │ └── ProjectSeeder.php ├── deployment ├── octane │ └── Swoole │ │ └── supervisord.swoole.conf ├── php.ini ├── start-container ├── supervisord.conf ├── supervisord.horizon.conf ├── supervisord.scheduler.conf ├── supervisord.worker.conf └── utilities.sh ├── docker-compose.prod.yml ├── docker-compose.yml ├── phpstan.neon ├── phpunit.xml ├── public ├── .htaccess ├── css │ ├── filament │ │ ├── filament │ │ │ └── app.css │ │ ├── forms │ │ │ └── forms.css │ │ └── support │ │ │ └── support.css │ ├── novadaemon │ │ └── filament-pretty-json │ │ │ └── styles.css │ └── rawilk │ │ └── filament-password-input │ │ └── filament-password-input.css ├── favicon.ico ├── index.php ├── js │ ├── filament │ │ ├── filament │ │ │ ├── app.js │ │ │ └── echo.js │ │ ├── forms │ │ │ └── components │ │ │ │ ├── color-picker.js │ │ │ │ ├── date-time-picker.js │ │ │ │ ├── file-upload.js │ │ │ │ ├── key-value.js │ │ │ │ ├── markdown-editor.js │ │ │ │ ├── rich-editor.js │ │ │ │ ├── select.js │ │ │ │ ├── tags-input.js │ │ │ │ └── textarea.js │ │ ├── notifications │ │ │ └── notifications.js │ │ ├── support │ │ │ ├── async-alpine.js │ │ │ └── support.js │ │ ├── tables │ │ │ └── components │ │ │ │ └── table.js │ │ └── widgets │ │ │ └── components │ │ │ ├── chart.js │ │ │ └── stats-overview │ │ │ └── stat │ │ │ └── chart.js │ └── novadaemon │ │ └── filament-pretty-json │ │ └── scripts.js ├── robots.txt └── vendor │ ├── horizon │ ├── app-dark.css │ ├── app.css │ ├── app.js │ ├── img │ │ ├── favicon.png │ │ ├── horizon.svg │ │ └── sprite.svg │ └── mix-manifest.json │ ├── log-viewer │ ├── app.css │ ├── app.js │ ├── app.js.LICENSE.txt │ ├── img │ │ ├── log-viewer-128.png │ │ ├── log-viewer-32.png │ │ └── log-viewer-64.png │ └── mix-manifest.json │ └── telescope │ ├── app-dark.css │ ├── app.css │ ├── app.js │ ├── favicon.ico │ └── mix-manifest.json ├── ray.php ├── resources └── views │ ├── .gitkeep │ └── vendor │ └── mail │ ├── html │ ├── button.blade.php │ ├── footer.blade.php │ ├── header.blade.php │ ├── layout.blade.php │ ├── message.blade.php │ ├── panel.blade.php │ ├── subcopy.blade.php │ ├── table.blade.php │ └── themes │ │ └── default.css │ └── text │ ├── button.blade.php │ ├── footer.blade.php │ ├── header.blade.php │ ├── layout.blade.php │ ├── message.blade.php │ ├── panel.blade.php │ ├── subcopy.blade.php │ └── table.blade.php ├── routes ├── api.php ├── auth.php ├── channels.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tests ├── CreatesApplication.php ├── Feature │ ├── AuthTest.php │ ├── CategoryTest.php │ ├── DomainTest.php │ ├── FileUploadTest.php │ ├── Project │ │ ├── ProjectImageTest.php │ │ ├── ProjectReportTest.php │ │ └── ProjectTest.php │ ├── ReleaseTest.php │ ├── Review │ │ ├── ReviewReportTest.php │ │ └── ReviewTest.php │ └── UserTest.php ├── Pest.php ├── TestCase.php └── Unit │ ├── ArchitectureTest.php │ ├── DomainServiceTest.php │ └── VersionNewTest.php └── wait-for-it.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/hot 4 | /public/storage 5 | /storage/*.key 6 | /storage/app/public 7 | /storage/debugbar 8 | /storage/ssr 9 | /storage/clockwork 10 | /storage/logs 11 | /storage/pail 12 | .phpunit.result.cache 13 | Homestead.json 14 | Homestead.yaml 15 | npm-debug.log 16 | yarn-error.log 17 | /vendor 18 | .env.backup 19 | /.idea/sonarlint 20 | .phpstorm.meta.php 21 | _ide_helper_models.php 22 | _ide_helper.php 23 | .php-cs-fixer.cache 24 | .husky 25 | /.vscode 26 | **/.DS_Store 27 | /public/page-cache 28 | .phpunit.database.checksum 29 | .phpunit.cache 30 | rr 31 | .rr.yaml 32 | frankenphp 33 | .config 34 | .data 35 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="FlorisBoard Addons" 2 | SESSION_COOKIE=addons_session 3 | APP_ENV=local 4 | APP_KEY= 5 | APP_DEBUG=true 6 | APP_URL=http://localhost 7 | FRONTEND_URL=http://localhost:3000 8 | SESSION_DOMAIN= 9 | 10 | LOG_CHANNEL=stack 11 | LOG_DEPRECATIONS_CHANNEL=null 12 | LOG_LEVEL=debug 13 | 14 | DB_CONNECTION=mysql 15 | DB_HOST=mysql 16 | DB_PORT=3306 17 | DB_DATABASE=addons_backend 18 | DB_USERNAME=sail 19 | DB_PASSWORD=password 20 | 21 | GITHUB_CLIENT_ID= 22 | GITHUB_CLIENT_SECRET= 23 | GITHUB_CLIENT_CALLBACK_URL= 24 | 25 | BROADCAST_DRIVER=log 26 | CACHE_DRIVER=redis 27 | FILESYSTEM_DISK=public 28 | QUEUE_CONNECTION=sync 29 | SESSION_DRIVER=file 30 | SESSION_LIFETIME=120 31 | 32 | MEMCACHED_HOST=127.0.0.1 33 | 34 | REDIS_HOST=redis 35 | REDIS_PASSWORD=null 36 | REDIS_PORT=6379 37 | 38 | MAIL_MAILER=smtp 39 | MAIL_HOST=mailpit 40 | MAIL_PORT=1025 41 | MAIL_USERNAME=null 42 | MAIL_PASSWORD=null 43 | MAIL_ENCRYPTION=null 44 | MAIL_FROM_ADDRESS="hello@example.com" 45 | MAIL_FROM_NAME="${APP_NAME}" 46 | 47 | PRIVATE_AWS_ACCESS_KEY_ID=sail 48 | PRIVATE_AWS_SECRET_ACCESS_KEY=password 49 | PRIVATE_AWS_DEFAULT_REGION=us-east-1 50 | PRIVATE_AWS_BUCKET=private 51 | PRIVATE_AWS_ENDPOINT=http://minio:9000 52 | PRIVATE_AWS_URL=http://localhost:9000/private 53 | PRIVATE_AWS_USE_PATH_STYLE_ENDPOINT=true 54 | 55 | PUBLIC_AWS_ACCESS_KEY_ID=sail 56 | PUBLIC_AWS_SECRET_ACCESS_KEY=password 57 | PUBLIC_AWS_DEFAULT_REGION=us-east-1 58 | PUBLIC_AWS_BUCKET=public 59 | PUBLIC_AWS_ENDPOINT=http://minio:9000 60 | PUBLIC_AWS_URL=http://localhost:9000/public 61 | PUBLIC_AWS_USE_PATH_STYLE_ENDPOINT=true 62 | 63 | PUSHER_APP_ID= 64 | PUSHER_APP_KEY= 65 | PUSHER_APP_SECRET= 66 | PUSHER_HOST= 67 | PUSHER_PORT=443 68 | PUSHER_SCHEME=https 69 | PUSHER_APP_CLUSTER=mt1 70 | 71 | VITE_APP_NAME="${APP_NAME}" 72 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 73 | VITE_PUSHER_HOST="${PUSHER_HOST}" 74 | VITE_PUSHER_PORT="${PUSHER_PORT}" 75 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 76 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 77 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /vendor 8 | .env 9 | .env.backup 10 | .env.production 11 | .phpunit.result.cache 12 | Homestead.json 13 | Homestead.yaml 14 | auth.json 15 | npm-debug.log 16 | yarn-error.log 17 | /.fleet 18 | /.idea 19 | /.vscode 20 | -------------------------------------------------------------------------------- /app/Console/Commands/DeleteTempUploadedFiles.php: -------------------------------------------------------------------------------- 1 | info("Checking $file"); 35 | if (now()->diffInDays($lastModified) > 3) { 36 | Storage::delete($file); 37 | $this->info("Deleted $file"); 38 | } 39 | } 40 | $endTime = microtime(true); 41 | $executionTime = $endTime - $startTime; 42 | 43 | $this->info("[{$executionTime}] Deleting temp files finished successfully."); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Console/Commands/DeleteUnverifiedDomains.php: -------------------------------------------------------------------------------- 1 | whereNull('verified_at') 31 | ->where('created_at', '<', now()->subDay()) 32 | ->delete(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command(DeleteTempUploadedFiles::class)->hourly(); 18 | $schedule->command('livewire:configure-s3-upload-cleanup')->hourly(); 19 | $schedule->command(DeleteUnverifiedDomains::class)->hourly(); 20 | } 21 | 22 | /** 23 | * Register the commands for the application. 24 | */ 25 | protected function commands(): void 26 | { 27 | $this->load(__DIR__.'/Commands'); 28 | 29 | require base_path('routes/console.php'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Enums/AuthProviderEnum.php: -------------------------------------------------------------------------------- 1 | name)->ucsplit()->join(' '); 19 | } 20 | 21 | public function getColor(): string 22 | { 23 | return 'primary'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Enums/ProjectTypeEnum.php: -------------------------------------------------------------------------------- 1 | name)->ucsplit()->join(' '); 19 | } 20 | 21 | public function getColor(): string 22 | { 23 | return 'primary'; 24 | } 25 | 26 | public function getValidationId(): string 27 | { 28 | return match ($this) { 29 | self::Theme => 'ime.extension.theme' 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Enums/ReportTypeEnum.php: -------------------------------------------------------------------------------- 1 | name)->ucsplit()->join(' '); 24 | } 25 | 26 | public function getColor(): string 27 | { 28 | return 'primary'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Enums/StatusEnum.php: -------------------------------------------------------------------------------- 1 | name)->ucsplit()->join(' '); 25 | } 26 | 27 | public function getColor(): string 28 | { 29 | return match ($this) { 30 | self::Draft, self::UnderReview => 'warning', 31 | self::Approved => 'primary', 32 | self::Rejected => 'danger', 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $dontFlash = [ 16 | 'current_password', 17 | 'password', 18 | 'password_confirmation', 19 | ]; 20 | 21 | /** 22 | * Register the exception handling callbacks for the application. 23 | */ 24 | public function register(): void 25 | { 26 | $this->reportable(function (Throwable $e) { 27 | // 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Filament/Custom/AutoRelationManager.php: -------------------------------------------------------------------------------- 1 | headerActions([ 55 | Tables\Actions\CreateAction::make(), 56 | ]) 57 | ->actions([ 58 | Tables\Actions\EditAction::make(), 59 | Tables\Actions\DeleteAction::make(), 60 | ]) 61 | ->bulkActions([ 62 | Tables\Actions\DeleteBulkAction::make(), 63 | ]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Filament/Custom/CustomResource.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public static function getEloquentQuery(): Builder 15 | { 16 | return parent::getEloquentQuery()->withoutGlobalScopes(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Filament/Forms/Components/FileInput.php: -------------------------------------------------------------------------------- 1 | disk(env('FILESYSTEM_DISK')) 13 | ->collection($name) 14 | ->multiple($isMultiple) 15 | ->reorderable($isMultiple) 16 | ->openable() 17 | ->downloadable(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Filament/Forms/Components/ImageInput.php: -------------------------------------------------------------------------------- 1 | image() 13 | ->imageEditor(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Filament/Forms/Layouts/BasicForm.php: -------------------------------------------------------------------------------- 1 | $components 12 | * @param array $sideComponents 13 | */ 14 | public static function make(Form $form, array $components = [], array $sideComponents = []): Form 15 | { 16 | 17 | return ComplexForm::make($form, [BasicSection::make($components)], $sideComponents) 18 | ->columns($form->getRecord() ? 3 : 2); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Filament/Forms/Layouts/BasicSection.php: -------------------------------------------------------------------------------- 1 | $components 11 | */ 12 | public static function make(array $components): Forms\Components\Section 13 | { 14 | return Forms\Components\Section::make() 15 | ->schema($components) 16 | ->columns(2); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Filament/Forms/Layouts/ComplexForm.php: -------------------------------------------------------------------------------- 1 | $components 12 | * @param array $sideComponents 13 | */ 14 | public static function make(Form $form, array $components = [], array $sideComponents = []): Form 15 | { 16 | array_unshift($sideComponents, TimestampsSection::make()); 17 | 18 | return $form->schema([MainGroup::make($components), SideGroup::make($sideComponents)]) 19 | ->columns(3); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Filament/Forms/Layouts/ImagesSection.php: -------------------------------------------------------------------------------- 1 | $components 12 | * @return Section 13 | */ 14 | public static function make(array $components = []): Forms\Components\Section 15 | { 16 | return Forms\Components\Section::make('Images') 17 | ->schema($components); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Filament/Forms/Layouts/MainGroup.php: -------------------------------------------------------------------------------- 1 | $components 11 | */ 12 | public static function make(array $components): Forms\Components\Group 13 | { 14 | return Forms\Components\Group::make($components) 15 | ->columnSpan(2) 16 | ->columns(2); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Filament/Forms/Layouts/SideGroup.php: -------------------------------------------------------------------------------- 1 | $components 11 | */ 12 | public static function make(array $components): Forms\Components\Group 13 | { 14 | return Forms\Components\Group::make($components)->columns(['lg' => 1]); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Filament/Forms/Layouts/StatusSection.php: -------------------------------------------------------------------------------- 1 | $components 12 | */ 13 | public static function make(array $components = [], bool $includeIsActive = false, bool $includeStatusSelect = false): Forms\Components\Section 14 | { 15 | if ($includeIsActive) { 16 | array_unshift( 17 | $components, 18 | Forms\Components\Toggle::make('is_active'), 19 | ); 20 | } 21 | 22 | if ($includeStatusSelect) { 23 | array_unshift( 24 | $components, 25 | Forms\Components\Select::make('status') 26 | ->searchable() 27 | ->options(StatusEnum::class) 28 | ->required(), 29 | ); 30 | } 31 | 32 | return Forms\Components\Section::make('Status') 33 | ->schema($components); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Filament/Forms/Layouts/TimestampsSection.php: -------------------------------------------------------------------------------- 1 | schema([ 13 | Forms\Components\Placeholder::make('created_at') 14 | ->content(fn ($record): ?string => $record->created_at?->diffForHumans()), 15 | 16 | Forms\Components\Placeholder::make('updated_at') 17 | ->content(fn ($record): ?string => $record->updated_at?->diffForHumans()), 18 | ]) 19 | ->hidden(fn ($record) => $record === null); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Filament/Resources/CategoryResource/Pages/CreateCategory.php: -------------------------------------------------------------------------------- 1 | actions([ 20 | Tables\Actions\EditAction::make(), 21 | Tables\Actions\DetachAction::make(), 22 | ]) 23 | ->bulkActions([ 24 | Tables\Actions\DetachBulkAction::make(), 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Filament/Resources/DomainResource/Pages/CreateDomain.php: -------------------------------------------------------------------------------- 1 | actions([ 20 | Tables\Actions\EditAction::make(), 21 | Tables\Actions\DetachAction::make(), 22 | ]) 23 | ->bulkActions([ 24 | Tables\Actions\DetachBulkAction::make(), 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Filament/Resources/ProjectResource/RelationManagers/ReleasesRelationManager.php: -------------------------------------------------------------------------------- 1 | actions([ 30 | Tables\Actions\EditAction::make(), 31 | Tables\Actions\DetachAction::make(), 32 | ]) 33 | ->bulkActions([ 34 | Tables\Actions\DetachBulkAction::make(), 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Filament/Resources/UserResource/RelationManagers/ProjectsRelationManager.php: -------------------------------------------------------------------------------- 1 | sortable()->dateTime()->toggleable(), 16 | Tables\Columns\TextColumn::make('updated_at')->sortable()->dateTime()->toggleable(), 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthController.php: -------------------------------------------------------------------------------- 1 | redirectTo(config('app.frontend_url').'/auth'); 16 | } 17 | 18 | public function destroy(Request $request): Response 19 | { 20 | Auth::guard('web')->logout(); 21 | 22 | $request->session()->invalidate(); 23 | 24 | $request->session()->regenerateToken(); 25 | 26 | return response()->noContent(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/GithubController.php: -------------------------------------------------------------------------------- 1 | setScopes($this->scopes) 22 | ->stateless() 23 | ->redirect() 24 | ->getTargetUrl(); 25 | 26 | return new JsonResponse(['url' => $url]); 27 | } 28 | 29 | public function callback(): RedirectResponse 30 | { 31 | $result = Socialite::with('github') 32 | ->setScopes($this->scopes) 33 | ->stateless() 34 | ->user(); 35 | 36 | $user = User::firstOrCreate([ 37 | 'provider_id' => $result->getId(), 38 | 'provider' => AuthProviderEnum::Github, 39 | ], [ 40 | 'username' => $result->getNickname(), 41 | 'password' => Str::password(12), 42 | ]); 43 | 44 | Auth::login($user, true); 45 | 46 | return response()->redirectToIntended(sprintf('%s/users/%s', config('app.frontend_url'), Auth::user()->username)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Http/Controllers/CategoryController.php: -------------------------------------------------------------------------------- 1 | > 18 | */ 19 | public function index(Request $request): AnonymousResourceCollection 20 | { 21 | $request->validate([ 22 | 'filter' => ['nullable', 'array'], 23 | 'filter.title' => ['nullable', 'string'], 24 | 'page' => ['nullable', 'integer'], 25 | ]); 26 | 27 | $categories = QueryBuilder::for(Category::class) 28 | ->allowedFilters([ 29 | AllowedFilter::partial('title'), 30 | ]) 31 | ->withGlobalScope('active', new ActiveScope) 32 | ->paginate(20); 33 | 34 | return CategoryResource::collection($categories); 35 | } 36 | 37 | public function show(Category $category): CategoryResource 38 | { 39 | return new CategoryResource($category); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Http/Controllers/CheckUpdateController.php: -------------------------------------------------------------------------------- 1 | validate([ 21 | 'projects' => ['required', 'array', 'min:1'], 22 | 'projects.*.package_name' => ['required', 'string', 'min:3', 'max:255', 'regex:'.StoreProjectRequest::$packageNameRegex], 23 | 'projects.*.version_name' => ['required', 'string', 'regex:'.StoreReleaseRequest::$versionNameRegex], 24 | ]); 25 | 26 | $packageNames = collect($request->input('projects'))->pluck('package_name'); 27 | $projects = Project::query() 28 | ->where('status', StatusEnum::Approved) 29 | ->whereIn('package_name', $packageNames) 30 | ->with('latestApprovedRelease.user') 31 | ->get(); 32 | 33 | return [ 34 | /** @var CheckUpdateResource[] */ 35 | 'data' => CheckUpdateResource::collection($projects), 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Controllers/CollectionController.php: -------------------------------------------------------------------------------- 1 | > 18 | */ 19 | public function index(Request $request): AnonymousResourceCollection 20 | { 21 | $request->validate([ 22 | 'filter' => ['nullable', 'array'], 23 | 'filter.title' => ['nullable', 'string'], 24 | 'filter.user_id' => ['nullable', 'numeric'], 25 | 'page' => ['nullable', 'integer'], 26 | ]); 27 | 28 | $collections = QueryBuilder::for(Collection::class) 29 | ->allowedFilters([ 30 | AllowedFilter::exact('user_id'), 31 | AllowedFilter::partial('title'), 32 | ]) 33 | ->with(['projects' => function (BelongsToMany $builder) { 34 | return $builder->with('image')->take(3); 35 | }]) 36 | ->withCount(['projects']) 37 | ->paginate(20); 38 | 39 | return CollectionResource::collection($collections); 40 | } 41 | 42 | public function store(Request $request): void 43 | { 44 | // 45 | } 46 | 47 | public function show(Collection $collection): void 48 | { 49 | // 50 | } 51 | 52 | public function update(Request $request, Collection $collection): void 53 | { 54 | // 55 | } 56 | 57 | public function destroy(Collection $collection): void 58 | { 59 | // 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | authorize('verify', $domain); 23 | 24 | if (! $this->domainService->hasVerificationText($domain->name, $domain->verification_code)) { 25 | throw ValidationException::withMessages(['message' => "Couldn't verify the domain please try again later."]); 26 | } 27 | 28 | $domain->update(['verified_at' => now()]); 29 | 30 | return new JsonResponse(['message' => 'Domain verified successfully.']); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Controllers/FileUploadController.php: -------------------------------------------------------------------------------- 1 | validate([ 15 | 'file' => ['required', File::default()->min('1kb')->max('512kb')], 16 | ]); 17 | 18 | /* @phpstan-ignore-next-line */ 19 | return $request->file('file')->storeAs($this->generatePath($request->file('file')->getClientOriginalExtension())); 20 | } 21 | 22 | public function generatePath(string $extension): string 23 | { 24 | return sprintf( 25 | 'tmp/%s-%s-%s.%s', 26 | Auth::id(), 27 | now()->timestamp, 28 | Str::random(20), 29 | Str::remove('.', $extension) 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Controllers/HomeController.php: -------------------------------------------------------------------------------- 1 | $this->categoryService->top(), 26 | /** @var ProjectResource[] */ 27 | 'picks_of_the_day' => $this->projectService->picksOfTheDay(), 28 | /** @var ProjectResource[] */ 29 | 'latest_releases' => $this->projectService->latestReleases(), 30 | /** @var ProjectResource[] */ 31 | 'latest_projects' => $this->projectService->latestProjects(), 32 | /** @var ProjectResource[] */ 33 | 'recommended' => $this->projectService->recommended(), 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Controllers/LoginAsController.php: -------------------------------------------------------------------------------- 1 | isLocal(), 404); 13 | 14 | $user = User::firstOrFail(); 15 | $user->update(['is_admin' => true]); 16 | Auth::loginUsingId($user->id); 17 | 18 | return "You're logged in as an admin."; 19 | } 20 | 21 | public function user(int $id): string 22 | { 23 | abort_unless(app()->isLocal(), 404); 24 | 25 | User::findOrFail($id); 26 | Auth::loginUsingId($id); 27 | 28 | return "You're logged in as $id."; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Controllers/Project/ProjectImageController.php: -------------------------------------------------------------------------------- 1 | authorize('updateImages', $project); 25 | 26 | $request->validate([ 27 | 'image_path' => ['bail', 'required', 'string', new FileUpload(['image/png', 'image/jpeg'])], 28 | ]); 29 | 30 | if ($project->status === StatusEnum::Draft) { 31 | $project 32 | ->addMediaFromDisk($request->input('image_path')) 33 | ->toMediaCollection('image'); 34 | } else { 35 | $changeProposal = $project->latestChangeProposal; 36 | $changeProposal->update(['data' => [ 37 | ...$changeProposal->data, 38 | 'image_path' => $request->input('image_path'), 39 | ]]); 40 | } 41 | 42 | return new JsonResponse(['message' => 'Image has been saved successfully.']); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Controllers/Project/ProjectReportController.php: -------------------------------------------------------------------------------- 1 | reports()->where('user_id', Auth::id())->latest('id')->first(); 19 | 20 | if ($previousReport && $previousReport->created_at->gt(now()->subHours(24))) { 21 | return new JsonResponse([ 22 | 'message' => 'You cannot report this project again so soon. Please wait until 24 hours after your last report.', 23 | ], 429); 24 | } 25 | 26 | $project->reports()->create([ 27 | ...$request->validated(), 28 | 'user_id' => Auth::id(), 29 | 'status' => StatusEnum::Draft, 30 | ]); 31 | 32 | return new JsonResponse([ 33 | 'message' => "You've reported the project $project->id successfully.", 34 | ], 201); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Controllers/Project/ScreenshotController.php: -------------------------------------------------------------------------------- 1 | authorize('updateImages', $project); 25 | 26 | $request->validate([ 27 | 'screenshots_path' => ['required', 'array', 'max:5'], 28 | 'screenshots_path.*' => ['bail', 'required', 'string', new FileUpload(['image/png', 'image/jpeg'])], 29 | ]); 30 | 31 | if ($project->status === StatusEnum::Draft) { 32 | foreach ($request->input('screenshots_path') as $screenshot) { 33 | $project->addMediaFromDisk($screenshot) 34 | ->toMediaCollection('screenshots'); 35 | } 36 | } else { 37 | $changeProposal = $project->latestChangeProposal; 38 | $changeProposal->update(['data' => [ 39 | ...$changeProposal->data, 40 | 'screenshots_path' => $request->input('screenshots_path'), 41 | ]]); 42 | } 43 | 44 | return new JsonResponse(['message' => 'Screenshots has been saved successfully.']); 45 | } 46 | 47 | /** 48 | * @throws AuthorizationException 49 | */ 50 | public function destroy(Project $project, int $media): JsonResponse 51 | { 52 | $this->authorize('deleteImages', $project); 53 | 54 | $project->screenshots() 55 | ->where('id', $media) 56 | ->first() 57 | ?->delete(); 58 | 59 | return new JsonResponse(['message' => 'Screenshot has been deleted successfully.']); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Http/Controllers/Review/ReviewReportController.php: -------------------------------------------------------------------------------- 1 | reports()->where('user_id', Auth::id())->latest('id')->first(); 19 | 20 | if ($previousReport && $previousReport->created_at->gt(now()->subHours(24))) { 21 | return new JsonResponse([ 22 | 'message' => 'You cannot report this review again so soon. Please wait until 24 hours after your last report.', 23 | ], 429); 24 | } 25 | 26 | $review->reports()->create([ 27 | ...$request->validated(), 28 | 'user_id' => Auth::id(), 29 | 'status' => StatusEnum::UnderReview, 30 | ]); 31 | 32 | return new JsonResponse([ 33 | 'message' => "You've reported the review $review->id successfully.", 34 | ], 201); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson() ? null : route('login'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/EnsureEmailIsVerified.php: -------------------------------------------------------------------------------- 1 | user() || 20 | ($request->user() instanceof MustVerifyEmail && 21 | ! $request->user()->hasVerifiedEmail())) { 22 | return response()->json(['message' => 'Your email address is not verified.'], 409); 23 | } 24 | 25 | return $next($request); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 24 | return redirect(RouteServiceProvider::HOME); 25 | } 26 | } 27 | 28 | return $next($request); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts(): array 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Middleware/ValidateSignature.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 'fbclid', 16 | // 'utm_campaign', 17 | // 'utm_content', 18 | // 'utm_medium', 19 | // 'utm_source', 20 | // 'utm_term', 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Requests/Project/UpdateProjectRequest.php: -------------------------------------------------------------------------------- 1 | |string> 26 | */ 27 | public function rules(): array 28 | { 29 | $base = [ 30 | 'category_id' => ['bail', 'required', 'numeric', Rule::exists(Category::class, 'id')], 31 | 'title' => ['required', 'string', 'min:3', 'max:255'], 32 | 'type' => ['required', Rule::enum(ProjectTypeEnum::class)], 33 | 'short_description' => ['required', 'string', 'min:3', 'max:255'], 34 | 'description' => ['required', 'string', 'min:3', 'max:1024'], 35 | 'links' => ['required', 'array:source_code'], 36 | 'links.source_code' => ['required', 'string', 'url', 'max:255'], 37 | ]; 38 | 39 | /** @var Project $project */ 40 | $project = $this->route('project'); 41 | 42 | if ($project?->user_id === Auth::Id()) { 43 | /** @var int[] */ 44 | $base['maintainers'] = ['bail', 'nullable', 'array', 'between:0,5']; 45 | $base['maintainers.*'] = ['bail', 'required', 'numeric', Rule::notIn([Auth::id()]), Rule::exists(User::class, 'id')]; 46 | } 47 | 48 | return $base; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Http/Requests/Release/StoreReleaseRequest.php: -------------------------------------------------------------------------------- 1 | |string> 26 | */ 27 | public function rules(): array 28 | { 29 | return [ 30 | 'description' => ['required', 'string', 'min:3', 'max:1024'], 31 | 'version_name' => ['required', 'string', 'regex:'.static::$versionNameRegex], 32 | 'file_path' => ['bail', 'required', new FileUpload(validExtensions: ['flex'])], 33 | ]; 34 | } 35 | 36 | /** 37 | * @return mixed[] 38 | */ 39 | public function after(): array 40 | { 41 | $base = [ 42 | new ValidateReleaseVersionName, 43 | ]; 44 | 45 | if (! app()->runningUnitTests()) { 46 | $base[] = new ValidateReleaseFile; 47 | } 48 | 49 | return $base; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Requests/ReviewRequest.php: -------------------------------------------------------------------------------- 1 | |string> 21 | */ 22 | public function rules(): array 23 | { 24 | return [ 25 | 'title' => ['required', 'string', 'min:3', 'max:50'], 26 | 'description' => ['required', 'string', 'min:3', 'max:1024'], 27 | 'score' => ['required', 'integer', 'between:1,5'], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreReportRequest.php: -------------------------------------------------------------------------------- 1 | |string> 23 | */ 24 | public function rules(): array 25 | { 26 | return [ 27 | 'type' => ['required', 'string', Rule::enum(ReportTypeEnum::class)], 28 | 'description' => ['required', 'string', 'min:3', 'max:2024'], 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Http/Resources/CategoryResource.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function toArray(Request $request): array 18 | { 19 | return [ 20 | 'id' => $this->id, 21 | 'title' => $this->title, 22 | 'is_top' => $this->is_top, 23 | 'circle_bg' => $this->circle_bg, 24 | 'circle_fg' => $this->circle_fg, 25 | 'icon_name' => $this->icon_name, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Resources/ChangeProposalResource.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function toArray(Request $request): array 19 | { 20 | // We only show the reviewerDescription when the change proposal is rejected 21 | $reviewerDescription = $this->status === StatusEnum::Rejected 22 | ? $this->reviewer_description 23 | : null; 24 | 25 | return [ 26 | 'id' => $this->id, 27 | /** @var StatusEnum */ 28 | 'status' => $this->status, 29 | /** @var string|null */ 30 | 'reviewer_description' => $reviewerDescription, 31 | 'data' => $this->data, 32 | /** @var string */ 33 | 'updated_at' => $this->updated_at, 34 | /** @var string */ 35 | 'created_at' => $this->created_at, 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Resources/CheckUpdateResource.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function toArray(Request $request): array 20 | { 21 | 22 | return [ 23 | 'project' => new ProjectSlimResource($this), 24 | /** @var ReleaseFullResource|null */ 25 | 'latest_release' => new ReleaseFullResource($this->latestApprovedRelease), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Resources/CollectionResource.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function toArray(Request $request): array 19 | { 20 | return [ 21 | 'id' => $this->id, 22 | 'user_id' => $this->user_id, 23 | 'title' => $this->title, 24 | 'is_public' => $this->is_public, 25 | /** @var string */ 26 | 'created_at' => $this->created_at, 27 | /** @var string */ 28 | 'updated_at' => $this->updated_at, 29 | 'user' => new UserResource($this->whenLoaded('user')), 30 | /** @var int */ 31 | 'projects_count' => $this->whenCounted('projects'), 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Http/Resources/DomainResource.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function toArray(Request $request): array 19 | { 20 | $domainService = app(DomainService::class); 21 | 22 | return [ 23 | 'id' => $this->id, 24 | 'name' => $this->name, 25 | 'verification_text' => $domainService->generateVerificationText($this->verification_code), 26 | 'verified_at' => $this->verified_at, 27 | /** @var bool */ 28 | 'is_reserved' => $domainService->isInExcludedDomains($this->name), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Http/Resources/Media/ImageResource.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function toArray(Request $request): array 18 | { 19 | return [ 20 | 'id' => $this->id, 21 | 'url' => $this->getFullUrl(), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Resources/Project/ProjectResource.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function toArray(Request $request): array 24 | { 25 | return [ 26 | 'id' => $this->id, 27 | 'category_id' => $this->category_id, 28 | 'user_id' => $this->user_id, 29 | 'title' => $this->title, 30 | 'package_name' => $this->package_name, 31 | 'short_description' => $this->short_description, 32 | 'type' => $this->type, 33 | 'is_recommended' => $this->is_recommended, 34 | /** @var StatusEnum */ 35 | 'status' => $this->status, 36 | /** @var string */ 37 | 'created_at' => $this->created_at, 38 | /** @var string */ 39 | 'updated_at' => $this->updated_at, 40 | /** @var ImageResource|null */ 41 | 'image' => new ImageResource($this->image), 42 | /** @var int */ 43 | 'reviews_avg_score' => round((int) $this->reviews_avg_score), 44 | /** @var int */ 45 | 'releases_sum_downloads_count' => (int) $this->releases_sum_downloads_count, 46 | /** @var ReleaseResource|null */ 47 | 'latest_release' => new ReleaseResource($this->latestApprovedRelease), 48 | /** @var int */ 49 | 'reviews_count' => $this->reviews_count, 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Http/Resources/Project/ProjectSlimResource.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function toArray(Request $request): array 19 | { 20 | return [ 21 | 'id' => $this->id, 22 | 'title' => $this->title, 23 | 'package_name' => $this->package_name, 24 | 'short_description' => $this->short_description, 25 | 'type' => $this->type, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Resources/Release/ReleaseFullResource.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function toArray(Request $request): array 20 | { 21 | return [ 22 | /** @var int */ 23 | 'id' => $this->id, 24 | /** @var int */ 25 | 'project_id' => $this->project_id, 26 | 'user_id' => $this->user_id, 27 | 'version_name' => $this->version_name, 28 | /** @var StatusEnum */ 29 | 'status' => $this->status, 30 | /** @var int */ 31 | 'version_code' => $this->version_code, 32 | 'description' => $this->description, 33 | /** @var int */ 34 | 'downloads_count' => round($this->downloads_count), 35 | 'download_link' => route('releases.download', $this), 36 | /** @var string */ 37 | 'created_at' => $this->created_at, 38 | /** @var string */ 39 | 'updated_at' => $this->updated_at, 40 | 'user' => new UserResource($this->user), 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Http/Resources/Release/ReleaseResource.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function toArray(Request $request): array 18 | { 19 | return [ 20 | 'id' => $this->id, 21 | 'version_name' => $this->version_name, 22 | /** @var int */ 23 | 'version_code' => $this->version_code, 24 | /** @var string */ 25 | 'created_at' => $this->created_at, 26 | /** @var string */ 27 | 'updated_at' => $this->updated_at, 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Resources/ReviewResource.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function toArray(Request $request): array 20 | { 21 | return [ 22 | 'id' => $this->id, 23 | 'title' => $this->title, 24 | 'description' => $this->description, 25 | /** @var int */ 26 | 'score' => $this->score, 27 | /** @var StatusEnum */ 28 | 'status' => $this->status, 29 | 'project_id' => $this->project_id, 30 | /** @var string */ 31 | 'created_at' => $this->created_at, 32 | /** @var string */ 33 | 'updated_at' => $this->updated_at, 34 | /** @var UserResource */ 35 | 'user' => new UserResource($this->user), 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Resources/User/AuthResource.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function toArray(Request $request): array 18 | { 19 | return [ 20 | /** @var int */ 21 | 'id' => $this->id, 22 | 'username' => $this->username, 23 | /** @var string|null */ 24 | 'username_changed_at' => $this->username_changed_at, 25 | /** @var string */ 26 | 'created_at' => $this->created_at, 27 | /** @var string */ 28 | 'updated_at' => $this->updated_at, 29 | /** @var bool */ 30 | 'can_view_admin' => $this->isAdministrator(), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Resources/User/UserResource.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function toArray(Request $request): array 18 | { 19 | return [ 20 | 'id' => $this->id, 21 | 'username' => $this->username, 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Models/Category.php: -------------------------------------------------------------------------------- 1 | 'boolean', 21 | ]; 22 | 23 | protected static function booted(): void 24 | { 25 | static::addGlobalScope(new ActiveScope); 26 | } 27 | 28 | /** 29 | * @return HasMany 30 | */ 31 | public function projects(): HasMany 32 | { 33 | return $this->hasMany(Project::class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Models/ChangeProposal.php: -------------------------------------------------------------------------------- 1 | StatusEnum::class, 20 | 'data' => 'array', 21 | ]; 22 | 23 | /** 24 | * @return BelongsTo 25 | */ 26 | public function user(): BelongsTo 27 | { 28 | return $this->belongsTo(User::class); 29 | } 30 | 31 | /** 32 | * @return MorphTo 33 | */ 34 | public function model(): MorphTo 35 | { 36 | return $this->morphTo('model'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Models/Collection.php: -------------------------------------------------------------------------------- 1 | 'boolean', 19 | ]; 20 | 21 | /** 22 | * @return BelongsTo 23 | */ 24 | public function user(): BelongsTo 25 | { 26 | return $this->belongsTo(User::class); 27 | } 28 | 29 | /** 30 | * @return BelongsToMany 31 | */ 32 | public function projects(): BelongsToMany 33 | { 34 | return $this->belongsToMany(Project::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Models/Domain.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected $casts = [ 22 | 'verified_at' => 'datetime', 23 | ]; 24 | 25 | /** 26 | * @return BelongsTo 27 | */ 28 | public function user(): BelongsTo 29 | { 30 | return $this->belongsTo(User::class); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Models/Maintainer.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function project(): BelongsTo 20 | { 21 | return $this->belongsTo(Project::class); 22 | } 23 | 24 | /** 25 | * @return BelongsTo 26 | */ 27 | public function user(): BelongsTo 28 | { 29 | return $this->belongsTo(User::class); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Models/Media.php: -------------------------------------------------------------------------------- 1 | StatusEnum::class, 22 | ]; 23 | 24 | public function registerMediaCollections(): void 25 | { 26 | $this->addMediaCollection('file') 27 | ->singleFile(); 28 | } 29 | 30 | /** 31 | * @return MorphOne 32 | */ 33 | public function file(): MorphOne 34 | { 35 | return $this->morphOne(Media::class, 'model') 36 | ->where('collection_name', 'file'); 37 | } 38 | 39 | /** 40 | * @return BelongsTo 41 | */ 42 | public function project(): BelongsTo 43 | { 44 | return $this->belongsTo(Project::class); 45 | } 46 | 47 | /** 48 | * @return BelongsTo 49 | */ 50 | public function user(): BelongsTo 51 | { 52 | return $this->belongsTo(User::class); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Models/Report.php: -------------------------------------------------------------------------------- 1 | ReportTypeEnum::class, 21 | 'status' => StatusEnum::class, 22 | ]; 23 | 24 | /** 25 | * @return BelongsTo 26 | */ 27 | public function user(): BelongsTo 28 | { 29 | return $this->belongsTo(User::class); 30 | } 31 | 32 | /** 33 | * @return MorphTo 34 | */ 35 | public function reportable(): MorphTo 36 | { 37 | return $this->morphTo(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Models/Review.php: -------------------------------------------------------------------------------- 1 | 'int', 21 | 'status' => StatusEnum::class, 22 | ]; 23 | 24 | /** 25 | * @return BelongsTo 26 | */ 27 | public function project(): BelongsTo 28 | { 29 | return $this->belongsTo(Project::class); 30 | } 31 | 32 | /** 33 | * @return BelongsTo 34 | */ 35 | public function user(): BelongsTo 36 | { 37 | return $this->belongsTo(User::class); 38 | } 39 | 40 | /** 41 | * @return MorphMany 42 | */ 43 | public function reports(): MorphMany 44 | { 45 | return $this->morphMany(Report::class, 'reportable'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Models/Scopes/ActiveScope.php: -------------------------------------------------------------------------------- 1 | $builder 15 | */ 16 | public function apply(Builder $builder, Model $model): void 17 | { 18 | $builder->where('is_active', true); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Observers/UserObserver.php: -------------------------------------------------------------------------------- 1 | domains()->firstOrCreate([ 22 | 'name' => sprintf('%s.github.io', Str::lower($user->username)), 23 | 'verified_at' => now(), 24 | 'verification_code' => $this->domainService->generateVerificationCode(), 25 | ]); 26 | } 27 | 28 | /** 29 | * Handle the User "updated" event. 30 | */ 31 | public function updated(User $user): void 32 | { 33 | // 34 | } 35 | 36 | /** 37 | * Handle the User "deleted" event. 38 | */ 39 | public function deleted(User $user): void 40 | { 41 | // 42 | } 43 | 44 | /** 45 | * Handle the User "restored" event. 46 | */ 47 | public function restored(User $user): void 48 | { 49 | // 50 | } 51 | 52 | /** 53 | * Handle the User "force deleted" event. 54 | */ 55 | public function forceDeleted(User $user): void 56 | { 57 | // 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Policies/ReviewPolicy.php: -------------------------------------------------------------------------------- 1 | project = $request->route('project'); 19 | } 20 | 21 | /** 22 | * Determine whether the user can view any models. 23 | */ 24 | public function viewAny(?User $user): bool 25 | { 26 | return true; 27 | } 28 | 29 | /** 30 | * Determine whether the user can view the model. 31 | */ 32 | public function view(?User $user, Review $review): bool 33 | { 34 | return true; 35 | } 36 | 37 | /** 38 | * Determine whether the user can create models. 39 | */ 40 | public function create(User $user): Response 41 | { 42 | return $user->reviews()->where('project_id', $this->project?->id)->exists() 43 | ? Response::deny('You already have a review for this project') 44 | : Response::allow(); 45 | } 46 | 47 | /** 48 | * Determine whether the user can update the model. 49 | */ 50 | public function update(User $user, Review $review): bool 51 | { 52 | return $user->id === $review->user_id; 53 | } 54 | 55 | /** 56 | * Determine whether the user can delete the model. 57 | */ 58 | public function delete(User $user, Review $review): bool 59 | { 60 | return $user->id === $review->user_id; 61 | } 62 | 63 | /** 64 | * Determine whether the user can restore the model. 65 | */ 66 | public function restore(User $user, Review $review): bool 67 | { 68 | return false; 69 | } 70 | 71 | /** 72 | * Determine whether the user can permanently delete the model. 73 | */ 74 | public function forceDelete(User $user, Review $review): bool 75 | { 76 | return false; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->isProduction()); 29 | JsonResource::withoutWrapping(); 30 | 31 | Password::defaults(function () { 32 | $rule = Password::min(8); 33 | 34 | if (! $this->app->isProduction()) { 35 | return $rule; 36 | } 37 | 38 | return $rule->letters() 39 | ->mixedCase() 40 | ->numbers() 41 | ->symbols() 42 | ->uncompromised(); 43 | }); 44 | 45 | if ($this->app->environment('local')) { 46 | $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class); 47 | $this->app->register(TelescopeServiceProvider::class); 48 | } 49 | 50 | Gate::before(function (?User $user) { 51 | if ($user?->isAdministrator()) { 52 | return true; 53 | } 54 | }); 55 | 56 | Gate::define('viewLogViewer', function (User $user) { 57 | return $user->isAdministrator(); 58 | }); 59 | 60 | Gate::define('viewApiDocs', function (?User $user) { 61 | return true; 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $policies = [ 17 | // 'App\Models\Model' => 'App\Policies\ModelPolicy', 18 | ]; 19 | 20 | /** 21 | * Register any authentication / authorization services. 22 | */ 23 | public function boot(): void 24 | { 25 | $this->registerPolicies(); 26 | 27 | ResetPassword::createUrlUsing(function (CanResetPassword $notifiable, string $token) { 28 | return config('app.frontend_url')."/reset-password/$token?email={$notifiable->getEmailForPasswordReset()}"; 29 | }); 30 | 31 | // 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 18 | */ 19 | protected $listen = [ 20 | Registered::class => [ 21 | SendEmailVerificationNotification::class, 22 | ], 23 | ]; 24 | 25 | /** 26 | * Register any events for your application. 27 | */ 28 | public function boot(): void 29 | { 30 | User::observe(UserObserver::class); 31 | } 32 | 33 | /** 34 | * Determine if events and listeners should be automatically discovered. 35 | */ 36 | public function shouldDiscoverEvents(): bool 37 | { 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Providers/Filament/AdminPanelProvider.php: -------------------------------------------------------------------------------- 1 | default() 27 | ->id('admin') 28 | ->path('admin') 29 | ->font('Poppins') 30 | ->colors([ 31 | 'primary' => Color::Green, 32 | 'orange' => Color::Orange, 33 | ]) 34 | ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') 35 | ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') 36 | ->pages([ 37 | Pages\Dashboard::class, 38 | ]) 39 | ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') 40 | ->widgets([ 41 | Widgets\AccountWidget::class, 42 | ]) 43 | ->middleware([ 44 | EncryptCookies::class, 45 | AddQueuedCookiesToResponse::class, 46 | StartSession::class, 47 | AuthenticateSession::class, 48 | ShareErrorsFromSession::class, 49 | VerifyCsrfToken::class, 50 | SubstituteBindings::class, 51 | DisableBladeIconComponents::class, 52 | DispatchServingFilamentEvent::class, 53 | ]) 54 | ->authMiddleware([ 55 | Authenticate::class, 56 | ]); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Providers/HorizonServiceProvider.php: -------------------------------------------------------------------------------- 1 | isAdministrator(); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | by($request->user()?->id ?: $request->ip()); 29 | }); 30 | 31 | RateLimiter::for('verifyDomain', function (Request $request) { 32 | return Limit::perMinute(8)->by($request->user()->id); 33 | }); 34 | 35 | RateLimiter::for('fileUpload', function (Request $request) { 36 | return Limit::perHour(20)->by($request->user()->id); 37 | }); 38 | 39 | $this->routes(function () { 40 | Route::middleware('api') 41 | ->prefix('api/v1') 42 | ->group(base_path('routes/api.php')); 43 | 44 | Route::middleware('web') 45 | ->group(base_path('routes/web.php')); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Providers/TelescopeServiceProvider.php: -------------------------------------------------------------------------------- 1 | hideSensitiveRequestDetails(); 20 | 21 | Telescope::filter(function (IncomingEntry $entry) { 22 | if ($this->app->environment('local')) { 23 | return true; 24 | } 25 | 26 | return $entry->isReportableException() || 27 | $entry->isFailedRequest() || 28 | $entry->isFailedJob() || 29 | $entry->isScheduledTask() || 30 | $entry->hasMonitoredTag(); 31 | }); 32 | } 33 | 34 | /** 35 | * Prevent sensitive request details from being logged by Telescope. 36 | */ 37 | protected function hideSensitiveRequestDetails(): void 38 | { 39 | if ($this->app->environment('local')) { 40 | return; 41 | } 42 | 43 | Telescope::hideRequestParameters(['_token']); 44 | 45 | Telescope::hideRequestHeaders([ 46 | 'cookie', 47 | 'x-csrf-token', 48 | 'x-xsrf-token', 49 | ]); 50 | } 51 | 52 | /** 53 | * Register the Telescope gate. 54 | * 55 | * This gate determines who can access Telescope in non-local environments. 56 | */ 57 | protected function gate(): void 58 | { 59 | Gate::define('viewTelescope', function ($user) { 60 | return false; 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Rules/FileUpload.php: -------------------------------------------------------------------------------- 1 | startsWith('tmp/')) { 31 | $fail('The path is not correct.'); 32 | } 33 | 34 | if (! empty($this->validExtensions) && ! in_array(explode('.', $value)[1], $this->validExtensions, true)) { 35 | $fail('The file has an invalid extension.'); 36 | } 37 | 38 | if (! Storage::exists($value)) { 39 | $fail('The file does not exist.'); 40 | } 41 | 42 | if (! empty($this->validMimeTypes) && ! in_array(Storage::mimeType($value), $this->validMimeTypes, true)) { 43 | $fail('The file is not a valid mime type.'); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Rules/Username.php: -------------------------------------------------------------------------------- 1 | addMinutes(5), function () { 15 | $projects = Category::query() 16 | ->ordered() 17 | ->where('is_top', true) 18 | ->get(); 19 | 20 | return CategoryResource::collection($projects); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Services/DomainService.php: -------------------------------------------------------------------------------- 1 | some(fn ($domain) => Str::endsWith($name, $domain)); 37 | } 38 | 39 | public function hasVerificationText(string $domain, string $verificationCode): bool 40 | { 41 | try { 42 | /** @var array[] $records */ 43 | $records = dns_get_record($domain, DNS_TXT); 44 | 45 | return collect($records)->some(function (array $record) use ($verificationCode) { 46 | return $record['host'] && $record['txt'] === static::generateVerificationText($verificationCode); 47 | }); 48 | } catch (\Exception $e) { 49 | return false; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Services/ReleaseService.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * @throws \JsonException 11 | * @throws \Throwable 12 | */ 13 | public function parseExtensionJson(string $tempDirPath): array|false 14 | { 15 | $finalPath = "$tempDirPath/extracted/extension.json"; 16 | 17 | if (! file_exists($finalPath)) { 18 | return false; 19 | } 20 | 21 | return json_decode(file_get_contents($finalPath), true, 124, JSON_THROW_ON_ERROR); 22 | } 23 | 24 | /** 25 | * @param array $result 26 | * 27 | * @throws \JsonException 28 | */ 29 | public function replaceExtensionJson(string $tempDirPath, array $result): bool 30 | { 31 | return (bool) file_put_contents("$tempDirPath/extracted/extension.json", json_encode($result, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Services/UserService.php: -------------------------------------------------------------------------------- 1 | subDays(14); 17 | 18 | if ($requiredPassedDays->lte($user->username_changed_at)) { 19 | throw ValidationException::withMessages([ 20 | 'username' => 'You cannot change your username for 14 days.', 21 | ]); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Validations/ValidateReleaseVersionName.php: -------------------------------------------------------------------------------- 1 | $part) { 20 | if ($part > $previousVersionParts[$index]) { 21 | return true; 22 | } 23 | 24 | if ($part < $previousVersionParts[$index]) { 25 | return false; 26 | } 27 | } 28 | 29 | return true; 30 | } 31 | 32 | public function __invoke(Validator $validator): void 33 | { 34 | 35 | /** @var Project $project */ 36 | $project = request()->route('project'); 37 | 38 | // It's the first release 39 | if (! $project->latestApprovedRelease) { 40 | return; 41 | } 42 | 43 | $previousVersionName = $project->latestApprovedRelease->version_name; 44 | 45 | $isValid = static::isProvidedVersionNewer($validator->getData()['version_name'], $previousVersionName); 46 | 47 | $validator->errors()->addIf(! $isValid, 'version_name', "The new release should be bigger than previous version which is $previousVersionName"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['*'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => true, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 12), 33 | 'verify' => true, 34 | ], 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Argon Options 39 | |-------------------------------------------------------------------------- 40 | | 41 | | Here you may specify the configuration options that should be used when 42 | | passwords are hashed using the Argon algorithm. These will allow you 43 | | to control the amount of time it takes to hash the given password. 44 | | 45 | */ 46 | 47 | 'argon' => [ 48 | 'memory' => 65536, 49 | 'threads' => 1, 50 | 'time' => 4, 51 | 'verify' => true, 52 | ], 53 | 54 | ]; 55 | -------------------------------------------------------------------------------- /config/query-builder.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'include' => 'include', 13 | 14 | 'filter' => 'filter', 15 | 16 | 'sort' => 'sort', 17 | 18 | 'fields' => 'fields', 19 | 20 | 'append' => 'append', 21 | ], 22 | 23 | /* 24 | * Related model counts are included using the relationship name suffixed with this string. 25 | * For example: GET /users?include=postsCount 26 | */ 27 | 'count_suffix' => 'Count', 28 | 29 | /* 30 | * Related model exists are included using the relationship name suffixed with this string. 31 | * For example: GET /users?include=postsExists 32 | */ 33 | 'exists_suffix' => 'Exists', 34 | 35 | /* 36 | * By default the package will throw an `InvalidFilterQuery` exception when a filter in the 37 | * URL is not allowed in the `allowedFilters()` method. 38 | */ 39 | 'disable_invalid_filter_query_exception' => false, 40 | 41 | /* 42 | * By default the package will throw an `InvalidSortQuery` exception when a sort in the 43 | * URL is not allowed in the `allowedSorts()` method. 44 | */ 45 | 'disable_invalid_sort_query_exception' => false, 46 | ]; 47 | -------------------------------------------------------------------------------- /config/scramble.php: -------------------------------------------------------------------------------- 1 | 'api', 11 | 12 | /* 13 | * Your API domain. By default, app domain is used. This is also a part of the default API routes 14 | * matcher, so when implementing your own, make sure you use this config if needed. 15 | */ 16 | 'api_domain' => null, 17 | 18 | 'info' => [ 19 | /* 20 | * API version. 21 | */ 22 | 'version' => env('API_VERSION', '1.0.0'), 23 | 24 | /* 25 | * Description rendered on the home page of the API documentation (`/docs/api`). 26 | */ 27 | 'description' => '', 28 | ], 29 | 30 | /* 31 | * Customize Stoplight Elements UI 32 | */ 33 | 'ui' => [ 34 | /* 35 | * Hide the `Try It` feature. Enabled by default. 36 | */ 37 | 'hide_try_it' => false, 38 | 39 | /* 40 | * URL to an image that displays as a small square logo next to the title, above the table of contents. 41 | */ 42 | 'logo' => '', 43 | ], 44 | 45 | /* 46 | * The list of servers of the API. By default, when `null`, server URL will be created from 47 | * `scramble.api_path` and `scramble.api_domain` config variables. When providing an array, you 48 | * will need to specify the local server URL manually (if needed). 49 | * 50 | * Example of non-default config (final URLs are generated using Laravel `url` helper): 51 | * 52 | * ```php 53 | * 'servers' => [ 54 | * 'Live' => 'api', 55 | * 'Prod' => 'https://scramble.dedoc.co/api', 56 | * ], 57 | * ``` 58 | */ 59 | 'servers' => null, 60 | 61 | 'middleware' => [ 62 | 'web', 63 | RestrictedDocsAccess::class, 64 | ], 65 | 66 | 'extensions' => [], 67 | ]; 68 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | 'scheme' => 'https', 22 | ], 23 | 24 | 'postmark' => [ 25 | 'token' => env('POSTMARK_TOKEN'), 26 | ], 27 | 28 | 'ses' => [ 29 | 'key' => env('AWS_ACCESS_KEY_ID'), 30 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 31 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 32 | ], 33 | 34 | 'github' => [ 35 | 'client_id' => env('GITHUB_CLIENT_ID'), 36 | 'client_secret' => env('GITHUB_CLIENT_SECRET'), 37 | 'redirect' => env('GITHUB_CLIENT_CALLBACK_URL'), 38 | ], 39 | 40 | ]; 41 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/CategoryFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class CategoryFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | 'title' => fake()->unique()->words(3, true), 21 | 'is_active' => app()->runningUnitTests() || fake()->boolean(90), 22 | 'is_top' => fake()->boolean(), 23 | 'circle_bg' => fake()->safeHexColor(), 24 | 'circle_fg' => fake()->safeHexColor(), 25 | 'icon_name' => 'HiFire', 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/factories/ChangeProposalFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class ChangeProposalFactory extends Factory 12 | { 13 | /** 14 | * Define the model's default state. 15 | * 16 | * @return array 17 | */ 18 | public function definition(): array 19 | { 20 | return [ 21 | 'status' => StatusEnum::randomValue(), 22 | 'reviewer_description' => fake()->boolean() ? fake()->paragraphs(rand(1, 3), true) : null, 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/factories/CollectionFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class CollectionFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | 'title' => fake()->words(rand(1, 6), true), 21 | 'is_public' => fake()->boolean(), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/factories/DomainFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class DomainFactory extends Factory 13 | { 14 | /** 15 | * Define the model's default state. 16 | * 17 | * @return array 18 | */ 19 | public function definition(): array 20 | { 21 | return [ 22 | 'name' => fake()->unique()->domainName(), 23 | 'verification_code' => rand(DomainService::MIN_VERIFICATION_CODE, DomainService::MAX_VERIFICATION_CODE), 24 | 'verified_at' => fake()->boolean(70) ? fake()->dateTime() : null, 25 | 'user_id' => User::factory(), 26 | ]; 27 | } 28 | 29 | /** 30 | * Indicate that the model's verified_at address should be now. 31 | */ 32 | public function verified(): static 33 | { 34 | return $this->state(fn (array $attributes) => [ 35 | 'verified_at' => now(), 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/factories/MaintainerFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class MaintainerFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | // 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /database/factories/MediaFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class MediaFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | 'uuid' => fake()->uuid(), 21 | 'name' => fake()->word(), 22 | 'file_name' => fake()->word(), 23 | 'mime_type' => fake()->mimeType(), 24 | 'disk' => fake()->word(), 25 | 'conversations_disk' => fake()->word(), 26 | 'size' => int(1_000, 10_000), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/factories/ReleaseFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class ReleaseFactory 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 | 'description' => fake()->realText(), 24 | 'version_name' => rand(0, 3).'.'.rand(0, 9).'.'.rand(0, 9), 25 | 'version_code' => rand(0, 1000), 26 | 'downloads_count' => rand(0, 1_000_000), 27 | 'status' => DatabaseSeeder::randomStatus(), 28 | 'user_id' => User::factory(), 29 | 'project_id' => Project::factory(), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/factories/ReportFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ReportFactory extends Factory 13 | { 14 | /** 15 | * Define the model's default state. 16 | * 17 | * @return array 18 | */ 19 | public function definition(): array 20 | { 21 | return [ 22 | 'type' => ReportTypeEnum::randomValue(), 23 | 'status' => DatabaseSeeder::randomStatus(), 24 | 'description' => fake()->realText(), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/factories/ReviewFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class ReviewFactory extends Factory 14 | { 15 | /** 16 | * Define the model's default state. 17 | * 18 | * @return array 19 | */ 20 | public function definition(): array 21 | { 22 | $deletedAt = fake()->boolean(20) ? fake()->dateTime() : null; 23 | 24 | return [ 25 | 'title' => fake()->words(rand(2, 5), true), 26 | 'description' => fake()->realText(), 27 | 'score' => rand(1, 5), 28 | 'status' => DatabaseSeeder::randomStatus(), 29 | 'deleted_at' => app()->runningUnitTests() ? null : $deletedAt, 30 | 'user_id' => User::factory(), 31 | 'project_id' => Project::factory(), 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class UserFactory extends Factory 14 | { 15 | protected static ?string $password; 16 | 17 | /** 18 | * Define the model's default state. 19 | * 20 | * @return array 21 | */ 22 | public function definition(): array 23 | { 24 | return [ 25 | 'username' => fake()->unique()->userName(), 26 | 'provider_id' => fake()->unique()->uuid(), 27 | 'provider' => AuthProviderEnum::randomValue(), 28 | 'username_changed_at' => fake()->boolean() ? fake()->dateTime() : null, 29 | 'password' => static::$password ??= Hash::make('password'), 30 | 'remember_token' => Str::random(10), 31 | 'is_admin' => false, 32 | ]; 33 | } 34 | 35 | /** 36 | * Indicate that the model's is_admin address should be true. 37 | */ 38 | public function admin(): static 39 | { 40 | return $this->state(fn (array $attributes) => [ 41 | 'is_admin' => true, 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('username')->unique(); 17 | $table->string('provider_id'); 18 | $table->string('provider'); 19 | $table->timestamp('username_changed_at')->nullable(); 20 | $table->boolean('is_admin')->default(false); 21 | $table->string('password'); 22 | $table->rememberToken(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('users'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php: -------------------------------------------------------------------------------- 1 | string('email')->primary(); 16 | $table->string('token'); 17 | $table->timestamp('created_at')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::dropIfExists('password_reset_tokens'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('uuid')->unique(); 17 | $table->text('connection'); 18 | $table->text('queue'); 19 | $table->longText('payload'); 20 | $table->longText('exception'); 21 | $table->timestamp('failed_at')->useCurrent(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('failed_jobs'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->morphs('tokenable'); 17 | $table->string('name'); 18 | $table->string('token', 64)->unique(); 19 | $table->text('abilities')->nullable(); 20 | $table->timestamp('last_used_at')->nullable(); 21 | $table->timestamp('expires_at')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('personal_access_tokens'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000002_create_sessions_table.php: -------------------------------------------------------------------------------- 1 | string('id')->primary(); 16 | $table->foreignId('user_id')->nullable()->index(); 17 | $table->string('ip_address', 45)->nullable(); 18 | $table->text('user_agent')->nullable(); 19 | $table->longText('payload'); 20 | $table->integer('last_activity')->index(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('sessions'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2022_12_14_083707_create_settings_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->string('group'); 15 | $table->string('name'); 16 | $table->boolean('locked')->default(false); 17 | $table->json('payload'); 18 | 19 | $table->timestamps(); 20 | 21 | $table->unique(['group', 'name']); 22 | }); 23 | } 24 | 25 | public function down() 26 | { 27 | Schema::dropIfExists('settings'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/2023_11_19_205100_create_categories_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('title'); 17 | $table->string('circle_bg'); 18 | $table->string('circle_fg'); 19 | $table->string('icon_name'); 20 | $table->unsignedBigInteger('order_column')->index(); 21 | $table->boolean('is_active'); 22 | $table->boolean('is_top'); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('categories'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2023_11_19_205104_create_projects_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); 17 | $table->foreignId('category_id')->nullable()->constrained()->nullOnDelete(); 18 | $table->string('title'); 19 | $table->string('package_name')->unique(); 20 | $table->string('type')->index(); 21 | $table->string('status')->index(); 22 | $table->text('description'); 23 | $table->string('short_description'); 24 | $table->json('links'); 25 | $table->boolean('is_recommended')->default(false); 26 | $table->softDeletes(); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | */ 34 | public function down(): void 35 | { 36 | Schema::dropIfExists('projects'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /database/migrations/2023_11_20_061313_create_media_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->morphs('model'); 15 | $table->uuid('uuid')->nullable()->unique(); 16 | $table->string('collection_name'); 17 | $table->string('name'); 18 | $table->string('file_name'); 19 | $table->string('mime_type')->nullable(); 20 | $table->string('disk'); 21 | $table->string('conversions_disk')->nullable(); 22 | $table->unsignedBigInteger('size'); 23 | $table->json('manipulations'); 24 | $table->json('custom_properties'); 25 | $table->json('generated_conversions'); 26 | $table->json('responsive_images'); 27 | $table->unsignedInteger('order_column')->nullable()->index(); 28 | 29 | $table->nullableTimestamps(); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2023_11_20_125316_create_maintainers_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id')->constrained()->cascadeOnDelete(); 17 | $table->foreignId('project_id')->constrained()->cascadeOnDelete(); 18 | $table->unique(['user_id', 'project_id']); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('maintainers'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_11_23_125556_create_collections_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id')->constrained()->cascadeOnDelete(); 17 | $table->string('title'); 18 | $table->boolean('is_public')->default(false); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('collections'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_11_23_132320_create_collection_project_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('collection_id')->constrained()->cascadeOnDelete(); 17 | $table->foreignId('project_id')->constrained()->cascadeOnDelete(); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('collection_project'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/2023_11_23_133825_create_releases_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('project_id')->constrained()->cascadeOnDelete(); 17 | $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); 18 | $table->string('version_name'); 19 | $table->unsignedBigInteger('version_code'); 20 | $table->string('status')->index(); 21 | $table->text('description')->nullable(); 22 | $table->unsignedBigInteger('downloads_count')->default(0); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('releases'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2023_11_24_124806_create_reviews_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('project_id')->constrained()->cascadeOnDelete(); 17 | $table->foreignId('user_id')->constrained()->cascadeOnDelete(); 18 | $table->string('title'); 19 | $table->string('status')->index(); 20 | $table->text('description'); 21 | $table->unsignedTinyInteger('score'); 22 | $table->softDeletes(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('reviews'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2023_11_29_115411_create_reports_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); 17 | $table->string('type')->index(); 18 | $table->string('status')->index(); 19 | $table->morphs('reportable'); 20 | $table->text('description'); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('reports'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2024_05_25_073616_create_domains_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id')->constrained()->cascadeOnDelete(); 17 | $table->string('name')->unique(); 18 | $table->string('verification_code'); 19 | $table->timestamp('verified_at')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('domains'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2024_06_26_200348_create_change_proposals_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->morphs('model'); 17 | $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); 18 | $table->string('status')->index(); 19 | $table->text('reviewer_description')->nullable(); 20 | $table->json('data'); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('change_proposals'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2024_10_14_193910_add_reviewer_description_to_projects_table.php: -------------------------------------------------------------------------------- 1 | text('reviewer_description')->nullable(); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('projects', function (Blueprint $table) { 25 | $table->drop('reviewer_description'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/seeders/CollectionSeeder.php: -------------------------------------------------------------------------------- 1 | each(function (User $user) use ($projects) { 21 | Collection::factory(rand(0, 5)) 22 | ->hasAttached($projects->random()) 23 | ->for($user) 24 | ->create(); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | randomNumber(1, 1000); 16 | 17 | return "https://picsum.photos/seed/$imageId/$with/$height"; 18 | } 19 | 20 | public static function randomStatus(): StatusEnum 21 | { 22 | return app()->runningUnitTests() 23 | ? StatusEnum::Approved 24 | : StatusEnum::from(StatusEnum::randomValue()); 25 | } 26 | 27 | /** 28 | * Seed the application's database. 29 | */ 30 | public function run(): void 31 | { 32 | User::factory(20)->hasDomains(5)->create(); 33 | Category::factory(20)->create(); 34 | 35 | $this->call([ 36 | ProjectSeeder::class, 37 | CollectionSeeder::class, 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /deployment/octane/Swoole/supervisord.swoole.conf: -------------------------------------------------------------------------------- 1 | [program:octane] 2 | process_name=%(program_name)s_%(process_num)02d 3 | command=php %(ENV_ROOT)s/artisan octane:start --server=swoole --host=0.0.0.0 --port=8000 4 | user=%(ENV_USER)s 5 | autostart=true 6 | autorestart=true 7 | environment=LARAVEL_OCTANE="1" 8 | stdout_logfile=/dev/stdout 9 | stdout_logfile_maxbytes=0 10 | stderr_logfile=/dev/stderr 11 | stderr_logfile_maxbytes=0 12 | 13 | [program:horizon] 14 | process_name=%(program_name)s_%(process_num)02d 15 | command=php %(ENV_ROOT)s/artisan horizon 16 | user=%(ENV_USER)s 17 | autostart=%(ENV_WITH_HORIZON)s 18 | autorestart=true 19 | stdout_logfile=%(ENV_ROOT)s/storage/logs/horizon.log 20 | stdout_logfile_maxbytes=200MB 21 | stderr_logfile=%(ENV_ROOT)s/storage/logs/horizon.log 22 | stderr_logfile_maxbytes=200MB 23 | stopwaitsecs=3600 24 | 25 | [program:scheduler] 26 | process_name=%(program_name)s_%(process_num)02d 27 | command=supercronic -overlapping /etc/supercronic/laravel 28 | user=%(ENV_USER)s 29 | autostart=%(ENV_WITH_SCHEDULER)s 30 | autorestart=true 31 | stdout_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log 32 | stdout_logfile_maxbytes=200MB 33 | stderr_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log 34 | stderr_logfile_maxbytes=200MB 35 | 36 | [program:clear-scheduler-cache] 37 | process_name=%(program_name)s_%(process_num)02d 38 | command=php %(ENV_ROOT)s/artisan schedule:clear-cache 39 | user=%(ENV_USER)s 40 | autostart=%(ENV_WITH_SCHEDULER)s 41 | autorestart=false 42 | startsecs=0 43 | startretries=1 44 | stdout_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log 45 | stdout_logfile_maxbytes=200MB 46 | stderr_logfile=%(ENV_ROOT)s/storage/logs/scheduler.log 47 | stderr_logfile_maxbytes=200MB 48 | 49 | [include] 50 | files=/etc/supervisor/supervisord.conf -------------------------------------------------------------------------------- /deployment/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | post_max_size = 100M 3 | upload_max_filesize = 100M 4 | expose_php = 0 5 | realpath_cache_size = 16M 6 | realpath_cache_ttl = 360 7 | 8 | [Opcache] 9 | opcache.enable = 1 10 | opcache.enable_cli = 1 11 | opcache.memory_consumption = 256M 12 | opcache.use_cwd = 0 13 | opcache.max_file_size = 0 14 | opcache.max_accelerated_files = 32531 15 | opcache.validate_timestamps = 0 16 | opcache.file_update_protection = 0 17 | opcache.interned_strings_buffer = 16 18 | opcache.file_cache = 60 19 | 20 | [JIT] 21 | opcache.jit_buffer_size = 128M 22 | opcache.jit = function 23 | opcache.jit_prof_threshold = 0.001 24 | opcache.jit_max_root_traces = 2048 25 | opcache.jit_max_side_traces = 256 26 | 27 | [zlib] 28 | zlib.output_compression = On 29 | zlib.output_compression_level = 9 30 | -------------------------------------------------------------------------------- /deployment/start-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | 4 | container_mode=${CONTAINER_MODE:-"http"} 5 | octane_server=${OCTANE_SERVER} 6 | running_migrations_and_seeders=${RUNNING_MIGRATIONS_AND_SEEDERS:-"false"} 7 | 8 | echo "Container mode: $container_mode" 9 | 10 | initialStuff() { 11 | php artisan storage:link; \ 12 | php artisan optimize:clear; \ 13 | php artisan event:cache; \ 14 | php artisan config:cache; \ 15 | php artisan route:cache; \ 16 | php artisan migrate --force; 17 | 18 | if [ ${running_migrations_and_seeders} = "true" ]; then 19 | echo "Running migrations and seeding database ..." 20 | php artisan migrate --isolated --seed --force; 21 | fi 22 | } 23 | 24 | if [ "$1" != "" ]; then 25 | exec "$@" 26 | elif [ ${container_mode} = "http" ]; then 27 | echo "Octane Server: $octane_server" 28 | initialStuff 29 | if [ ${octane_server} = "frankenphp" ]; then 30 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.frankenphp.conf 31 | elif [ ${octane_server} = "swoole" ]; then 32 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.swoole.conf 33 | elif [ ${octane_server} = "roadrunner" ]; then 34 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.roadrunner.conf 35 | else 36 | echo "Invalid Octane server supplied." 37 | exit 1 38 | fi 39 | elif [ ${container_mode} = "horizon" ]; then 40 | initialStuff 41 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.horizon.conf 42 | elif [ ${container_mode} = "scheduler" ]; then 43 | initialStuff 44 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.scheduler.conf 45 | elif [ ${container_mode} = "worker" ]; then 46 | initialStuff 47 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.worker.conf 48 | else 49 | echo "Container mode mismatched." 50 | exit 1 51 | fi 52 | -------------------------------------------------------------------------------- /deployment/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=%(ENV_USER)s 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [unix_http_server] 8 | file=/var/run/supervisor.sock 9 | 10 | [supervisorctl] 11 | serverurl=unix:///var/run/supervisor.sock 12 | 13 | [rpcinterface:supervisor] 14 | supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface -------------------------------------------------------------------------------- /deployment/supervisord.horizon.conf: -------------------------------------------------------------------------------- 1 | [program:horizon] 2 | process_name=%(program_name)s_%(process_num)02d 3 | command=php %(ENV_ROOT)s/artisan horizon 4 | user=%(ENV_USER)s 5 | autostart=true 6 | autorestart=true 7 | stdout_logfile=/dev/stdout 8 | stdout_logfile_maxbytes=0 9 | stderr_logfile=/dev/stderr 10 | stderr_logfile_maxbytes=0 11 | stopwaitsecs=3600 12 | 13 | [include] 14 | files=/etc/supervisor/supervisord.conf -------------------------------------------------------------------------------- /deployment/supervisord.scheduler.conf: -------------------------------------------------------------------------------- 1 | [program:scheduler] 2 | process_name=%(program_name)s_%(process_num)02d 3 | command=supercronic -overlapping /etc/supercronic/laravel 4 | user=%(ENV_USER)s 5 | autostart=true 6 | autorestart=true 7 | stdout_logfile=/dev/stdout 8 | stdout_logfile_maxbytes=0 9 | stderr_logfile=/dev/stderr 10 | stderr_logfile_maxbytes=0 11 | 12 | [program:clear-scheduler-cache] 13 | process_name=%(program_name)s_%(process_num)02d 14 | command=php %(ENV_ROOT)s/artisan schedule:clear-cache 15 | user=%(ENV_USER)s 16 | autostart=true 17 | autorestart=false 18 | startsecs=0 19 | startretries=1 20 | stdout_logfile=/dev/stdout 21 | stdout_logfile_maxbytes=0 22 | stderr_logfile=/dev/stderr 23 | stderr_logfile_maxbytes=0 24 | 25 | [include] 26 | files=/etc/supervisor/supervisord.conf -------------------------------------------------------------------------------- /deployment/supervisord.worker.conf: -------------------------------------------------------------------------------- 1 | [program:worker] 2 | process_name=%(program_name)s_%(process_num)02d 3 | command=%(ENV_WORKER_COMMAND)s 4 | user=%(ENV_USER)s 5 | autostart=true 6 | autorestart=true 7 | stdout_logfile=/dev/stdout 8 | stdout_logfile_maxbytes=0 9 | stderr_logfile=/dev/stderr 10 | stderr_logfile_maxbytes=0 11 | 12 | [include] 13 | files=/etc/supervisor/supervisord.conf -------------------------------------------------------------------------------- /deployment/utilities.sh: -------------------------------------------------------------------------------- 1 | tinker() { 2 | if [ -z "$1" ]; then 3 | php artisan tinker 4 | else 5 | php artisan tinker --execute="\"dd($1);\"" 6 | fi 7 | } 8 | 9 | # Commonly used aliases 10 | alias ..="cd .." 11 | alias ...="cd ../.." 12 | alias art="php artisan" 13 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | dns: 4 | - 8.8.8.8 5 | - 8.8.4.4 6 | build: 7 | context: . 8 | dockerfile: api.dockerfile 9 | networks: 10 | - addons 11 | environment: 12 | - CONTAINER_MODE=http 13 | - WITH_HORIZON=true 14 | - WITH_SCHEDULER=true 15 | env_file: 16 | - .env 17 | ports: 18 | - "8000:8000" 19 | depends_on: 20 | - pgsql 21 | - redis 22 | pgsql: 23 | image: postgres:16 24 | restart: unless-stopped 25 | volumes: 26 | - pgsql-data:/var/lib/postgresql/data 27 | ports: 28 | - "5432:5432" 29 | environment: 30 | POSTGRES_DB: '${DB_DATABASE}' 31 | POSTGRES_USER: '${DB_USERNAME}' 32 | POSTGRES_PASSWORD: '${DB_PASSWORD}' 33 | PGPASSWORD: '${DB_PASSWORD}' 34 | networks: 35 | - addons 36 | healthcheck: 37 | test: 38 | - CMD 39 | - pg_isready 40 | - '-q' 41 | - '-d' 42 | - '${DB_DATABASE}' 43 | - '-U' 44 | - '${DB_USERNAME}' 45 | retries: 3 46 | timeout: 5s 47 | redis: 48 | image: redis:alpine 49 | restart: unless-stopped 50 | networks: 51 | - addons 52 | volumes: 53 | - redis-data:/data 54 | ports: 55 | - "6379:6379" 56 | healthcheck: 57 | test: 58 | - CMD 59 | - redis-cli 60 | - ping 61 | retries: 3 62 | timeout: 5s 63 | minio: 64 | image: 'minio/minio:latest' 65 | ports: 66 | - "9000:9000" 67 | - "8900:8900" 68 | environment: 69 | MINIO_ROOT_USER: '${PUBLIC_AWS_ACCESS_KEY_ID}' 70 | MINIO_ROOT_PASSWORD: '${PUBLIC_AWS_SECRET_ACCESS_KEY}' 71 | volumes: 72 | - 'minio-data:/data/minio' 73 | networks: 74 | - addons 75 | command: 'minio server /data/minio --console-address ":8900"' 76 | healthcheck: 77 | test: 78 | - CMD 79 | - curl 80 | - '-f' 81 | - 'http://localhost:9000/minio/health/live' 82 | retries: 3 83 | timeout: 5s 84 | 85 | volumes: 86 | storage: 87 | minio-data: 88 | pgsql-data: 89 | redis-data: 90 | 91 | networks: 92 | addons: 93 | name: addons 94 | external: true 95 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/larastan/larastan/extension.neon 3 | 4 | parameters: 5 | 6 | paths: 7 | - app/ 8 | 9 | # Level 9 is the highest level 10 | level: 7 11 | 12 | scanFiles: 13 | - _ide_helper_models.php 14 | 15 | # ignoreErrors: 16 | # - '#PHPDoc tag @var#' 17 | # 18 | # excludePaths: 19 | # - ./*/*/FileToBeExcluded.php 20 | # 21 | # checkMissingIterableValueType: false 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/css/novadaemon/filament-pretty-json/styles.css: -------------------------------------------------------------------------------- 1 | pre.prettyjson { 2 | color: black; 3 | background-color: rgba(0, 0, 0, 0); 4 | border: 1px solid rgb(229, 231, 235); 5 | border-radius: 0.5rem; 6 | padding: 10px 20px; 7 | overflow: auto; 8 | font-size: 12px; 9 | } 10 | 11 | :is(.dark) pre.prettyjson { 12 | opacity: .7; 13 | --tw-bg-opacity: 1; 14 | --tw-border-opacity: 1; 15 | border: 1px solid rgb(75 85 99/var(--tw-border-opacity)); 16 | color: rgb(209 213 219/var(--tw-text-opacity)); 17 | } 18 | 19 | :is(.dark) pre.prettyjson span.json-key { 20 | color: red !important; 21 | } 22 | 23 | :is(.dark) pre.prettyjson span.json-string { 24 | color: aquamarine !important; 25 | } 26 | 27 | :is(.dark) pre.prettyjson span.json-value { 28 | color: deepskyblue !important; 29 | } 30 | 31 | .copy-button { 32 | position: absolute; 33 | right: 5px; 34 | top: 5px; 35 | width: 20px; 36 | height: 20px; 37 | text-align: center; 38 | line-height: 20px; 39 | cursor: pointer; 40 | color: rgb(156 163 175); 41 | border: none; 42 | outline: none; 43 | } 44 | 45 | .copy-button:hover { 46 | color: rgb(75 85 99); 47 | } 48 | 49 | .copy-button:active, .copy-button:focus { 50 | border: none; 51 | outline: none; 52 | } 53 | -------------------------------------------------------------------------------- /public/css/rawilk/filament-password-input/filament-password-input.css: -------------------------------------------------------------------------------- 1 | .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.relative{position:relative}.flex{display:flex}.h-5{height:1.25rem}.w-5{width:1.25rem}.overflow-hidden{overflow:hidden}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}:is([dir=ltr] .ltr\:mr-2){margin-right:.5rem}:is([dir=ltr] .ltr\:mr-2\.5){margin-right:.625rem}:is([dir=rtl] .rtl\:ml-2){margin-left:.5rem}:is([dir=rtl] .rtl\:ml-2\.5){margin-left:.625rem}:is(.dark .dark\:text-gray-400){--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))} -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/florisboard/addons-backend/5b9b42a4d939221c9b80a050f99f34e6bffbae48/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/key-value.js: -------------------------------------------------------------------------------- 1 | function r({state:o}){return{state:o,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(t,e)=>{let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows);this.rows=[];let s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.$nextTick(()=>{this.rows=e,this.updateState()})},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default}; 2 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/tags-input.js: -------------------------------------------------------------------------------- 1 | function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{"x-on:blur":"createTag()","x-model":"newTag","x-on:keydown"(t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},"x-on:paste"(){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default}; 2 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/textarea.js: -------------------------------------------------------------------------------- 1 | function r({initialHeight:t,shouldAutosize:i,state:s}){return{state:s,wrapperEl:null,init:function(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),i?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight:function(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=t+"rem")},resize:function(){if(this.setInitialHeight(),this.$el.scrollHeight<=0)return;let e=this.$el.scrollHeight+"px";this.wrapperEl.style.height!==e&&(this.wrapperEl.style.height=e)},setUpResizeObserver:function(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{r as default}; 2 | -------------------------------------------------------------------------------- /public/js/novadaemon/filament-pretty-json/scripts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pretty Print JSON Objects. 3 | * Inspired by http://jsfiddle.net/unLSJ/ 4 | * 5 | * @return {string} html string of the formatted JS object 6 | * @example: var obj = {"foo":"bar"}; obj.prettyPrint(); 7 | */ 8 | window.prettyPrint = function (json) { 9 | var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*|\[\])?([,[{])?$/mg; 10 | var replacer = function (match, pIndent, pKey, pVal, pEnd) { 11 | var key = '', 12 | val = '', 13 | str = '', 14 | r = pIndent || ''; 15 | if (pKey) 16 | r = r + key + pKey.replace(/[": ]/g, '') + ': '; 17 | if (pVal) 18 | r = r + (pVal[0] == '"' ? str : val) + pVal + ''; 19 | return r + (pEnd || ''); 20 | }; 21 | 22 | return JSON.stringify(json, null, 3) 23 | .replace(/&/g, '&').replace(/\\"/g, '"') 24 | .replace(//g, '>') 25 | .replace(jsonLine, replacer); 26 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/vendor/horizon/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/florisboard/addons-backend/5b9b42a4d939221c9b80a050f99f34e6bffbae48/public/vendor/horizon/img/favicon.png -------------------------------------------------------------------------------- /public/vendor/horizon/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=b4f3f08e60211bd6948ec35e5e9de9a1", 3 | "/app-dark.css": "/app-dark.css?id=15c72df05e2b1147fa3e4b0670cfb435", 4 | "/app.css": "/app.css?id=4d6a1a7fe095eedc2cb2a4ce822ea8a5", 5 | "/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f", 6 | "/img/horizon.svg": "/img/horizon.svg?id=904d5b5185fefb09035384e15bfca765", 7 | "/img/sprite.svg": "/img/sprite.svg?id=afc4952b74895bdef3ab4ebe9adb746f" 8 | } 9 | -------------------------------------------------------------------------------- /public/vendor/log-viewer/app.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */ 7 | 8 | /*! 9 | * pinia v2.2.2 10 | * (c) 2024 Eduardo San Martin Morote 11 | * @license MIT 12 | */ 13 | 14 | /*! #__NO_SIDE_EFFECTS__ */ 15 | 16 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 17 | 18 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 19 | 20 | /** 21 | * @license 22 | * Lodash 23 | * Copyright OpenJS Foundation and other contributors 24 | * Released under MIT license 25 | * Based on Underscore.js 1.8.3 26 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 27 | */ 28 | 29 | /** 30 | * @vue/shared v3.4.38 31 | * (c) 2018-present Yuxi (Evan) You and Vue contributors 32 | * @license MIT 33 | **/ 34 | -------------------------------------------------------------------------------- /public/vendor/log-viewer/img/log-viewer-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/florisboard/addons-backend/5b9b42a4d939221c9b80a050f99f34e6bffbae48/public/vendor/log-viewer/img/log-viewer-128.png -------------------------------------------------------------------------------- /public/vendor/log-viewer/img/log-viewer-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/florisboard/addons-backend/5b9b42a4d939221c9b80a050f99f34e6bffbae48/public/vendor/log-viewer/img/log-viewer-32.png -------------------------------------------------------------------------------- /public/vendor/log-viewer/img/log-viewer-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/florisboard/addons-backend/5b9b42a4d939221c9b80a050f99f34e6bffbae48/public/vendor/log-viewer/img/log-viewer-64.png -------------------------------------------------------------------------------- /public/vendor/log-viewer/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=c5e0f20ee42d437f446958f2c1001581", 3 | "/app.css": "/app.css?id=5593a0331dd40729ff41e32a6035d872", 4 | "/img/log-viewer-128.png": "/img/log-viewer-128.png?id=d576c6d2e16074d3f064e60fe4f35166", 5 | "/img/log-viewer-32.png": "/img/log-viewer-32.png?id=f8ec67d10f996aa8baf00df3b61eea6d", 6 | "/img/log-viewer-64.png": "/img/log-viewer-64.png?id=8902d596fc883ca9eb8105bb683568c6" 7 | } 8 | -------------------------------------------------------------------------------- /public/vendor/telescope/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/florisboard/addons-backend/5b9b42a4d939221c9b80a050f99f34e6bffbae48/public/vendor/telescope/favicon.ico -------------------------------------------------------------------------------- /public/vendor/telescope/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=99e99836705c54c9dc04352a9907bc7f", 3 | "/app-dark.css": "/app-dark.css?id=1ea407db56c5163ae29311f1f38eb7b9", 4 | "/app.css": "/app.css?id=de4c978567bfd90b38d186937dee5ccf" 5 | } 6 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/button.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'url', 3 | 'color' => 'primary', 4 | 'align' => 'center', 5 | ]) 6 | 7 | 8 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/footer.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/header.blade.php: -------------------------------------------------------------------------------- 1 | @props(['url']) 2 | 3 | 4 | 5 | @if (trim($slot) === 'Laravel') 6 | 7 | @else 8 | {{ $slot }} 9 | @endif 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/message.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{-- Header --}} 3 | 4 | 5 | {{ config('app.name') }} 6 | 7 | 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | 15 | 16 | {{ $subcopy }} 17 | 18 | 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | 23 | 24 | © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/panel.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/table.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{ Illuminate\Mail\Markdown::parse($slot) }} 3 |
4 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/button.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }}: {{ $url }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/footer.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/header.blade.php: -------------------------------------------------------------------------------- 1 | [{{ $slot }}]({{ $url }}) 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/layout.blade.php: -------------------------------------------------------------------------------- 1 | {!! strip_tags($header ?? '') !!} 2 | 3 | {!! strip_tags($slot) !!} 4 | @isset($subcopy) 5 | 6 | {!! strip_tags($subcopy) !!} 7 | @endisset 8 | 9 | {!! strip_tags($footer ?? '') !!} 10 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/message.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{-- Header --}} 3 | 4 | 5 | {{ config('app.name') }} 6 | 7 | 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | 15 | 16 | {{ $subcopy }} 17 | 18 | 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | 23 | 24 | © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/panel.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/table.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /routes/auth.php: -------------------------------------------------------------------------------- 1 | group(function () { 9 | Route::post('/logout', [AuthController::class, 'destroy']) 10 | ->middleware('auth') 11 | ->name('logout'); 12 | 13 | Route::get('github/redirect', [GithubController::class, 'redirect']) 14 | ->name('github.redirect'); 15 | 16 | Route::get('github/callback', [GithubController::class, 'callback']) 17 | ->name('github.callback'); 18 | }); 19 | 20 | Route::get('login', [AuthController::class, 'login']) 21 | ->middleware('guest') 22 | ->name('login'); 23 | 24 | Route::get('login-as-admin', [LoginAsController::class, 'admin']); 25 | Route::get('login-as-{id}', [LoginAsController::class, 'user']); 26 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 18 | 19 | return $app; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Feature/AuthTest.php: -------------------------------------------------------------------------------- 1 | actingAs(User::factory()->create()) 7 | ->post(route('logout')) 8 | ->assertNoContent(); 9 | 10 | $this->assertGuest(); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/Feature/CategoryTest.php: -------------------------------------------------------------------------------- 1 | group('Category'); 6 | 7 | test('users can get categories', function () { 8 | Category::factory(2)->create(); 9 | 10 | $this->getJson(route('categories.index')) 11 | ->assertOk(); 12 | }); 13 | 14 | test('users can get an active category', function () { 15 | $category = Category::factory()->create(['is_active' => true]); 16 | 17 | $this->getJson(route('categories.show', [$category])) 18 | ->assertOk(); 19 | }); 20 | 21 | test('users cannot get an inactive blog category', function () { 22 | $category = Category::factory()->create(['is_active' => false]); 23 | 24 | $this->getJson(route('categories.show', [$category])) 25 | ->assertNotFound(); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/Feature/FileUploadTest.php: -------------------------------------------------------------------------------- 1 | create()); 11 | Storage::fake(); 12 | 13 | $this->freezeTime(); 14 | Str::createRandomStringsUsing(static fn (): string => 'random'); 15 | }); 16 | 17 | test('users can upload', function () { 18 | $file = UploadedFile::fake()->image('image.png')->size(5); 19 | 20 | $path = App::make(FileUploadController::class)->generatePath('png'); 21 | 22 | $this->postJson(route('uploads.process'), ['file' => $file]) 23 | ->assertOk() 24 | ->assertSee($path); 25 | 26 | Storage::assertExists($path); 27 | }); 28 | 29 | test('users should send a file', function () { 30 | $this->postJson(route('uploads.process')) 31 | ->assertUnprocessable(); 32 | }); 33 | 34 | test('users cannot send a file bigger than 512KB', function () { 35 | $file = UploadedFile::fake()->image('image.png')->size(600); 36 | 37 | $this->postJson(route('uploads.process'), ['file' => $file]) 38 | ->assertUnprocessable(); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/Feature/Project/ProjectImageTest.php: -------------------------------------------------------------------------------- 1 | group('ProjectImage'); 9 | 10 | describe('Store', function () { 11 | test('users can create project image', function () { 12 | Sanctum::actingAs($user = User::factory()->create()); 13 | $project = Project::factory()->for($user)->create(['status' => StatusEnum::Draft]); 14 | $data = ['image_path' => createUploadedImage()]; 15 | 16 | $this->postJson(route('projects.image.store', [$project]), $data) 17 | ->assertOk(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/Feature/Project/ProjectReportTest.php: -------------------------------------------------------------------------------- 1 | group('ProjectReport'); 9 | 10 | describe('Create', function () { 11 | test('users can report a project', function () { 12 | Sanctum::actingAs(User::factory()->create()); 13 | $project = Project::factory()->create(); 14 | $data = Report::factory()->make()->toArray(); 15 | 16 | $this->postJson(route('projects.reports.store', $project), $data) 17 | ->assertCreated(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/Feature/ReleaseTest.php: -------------------------------------------------------------------------------- 1 | group('Release'); 11 | 12 | describe('Index', function () { 13 | test('users can get releases', function () { 14 | Release::factory()->create(); 15 | 16 | $this->getJson(route('reviews.index')) 17 | ->assertOk(); 18 | }); 19 | }); 20 | 21 | describe('Download', function () { 22 | test('users can download a release', function () { 23 | Storage::fake(); 24 | 25 | $release = Release::factory()->create(['status' => StatusEnum::Approved]); 26 | 27 | $file = UploadedFile::fake()->create('file.flex', 1); 28 | $release->addMedia($file)->toMediaCollection('file'); 29 | 30 | $this->getJson(route('releases.download', [$release])) 31 | ->assertOk(); 32 | 33 | $this->assertDatabaseHas(Release::class, [ 34 | 'id' => $release->id, 35 | 'downloads_count' => $release->downloads_count + 1, 36 | ]); 37 | }); 38 | }); 39 | 40 | describe('Create', function () { 41 | test('users can create a release', function () { 42 | Sanctum::actingAs($user = User::factory()->create()); 43 | $project = Project::factory() 44 | ->has(Release::factory(['version_name' => '1.0.0'])) 45 | ->for($user) 46 | ->create(); 47 | 48 | $data = [ 49 | ...Release::factory()->make()->toArray(), 50 | 'version_name' => '2.0.0', 51 | 'file_path' => createUploadedFile('file.flex'), 52 | ]; 53 | 54 | $this->postJson(route('projects.releases.store', [$project]), $data) 55 | ->assertCreated(); 56 | }); 57 | 58 | test('users cannot create a release from another project', function () { 59 | Sanctum::actingAs(User::factory()->create()); 60 | $project = Project::factory()->create(); 61 | 62 | $this->postJson(route('projects.releases.store', [$project])) 63 | ->assertForbidden(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/Feature/Review/ReviewReportTest.php: -------------------------------------------------------------------------------- 1 | group('ReviewReport'); 9 | 10 | describe('Create', function () { 11 | test('users can report a review', function () { 12 | Sanctum::actingAs(User::factory()->create()); 13 | $review = Review::factory()->create(); 14 | $data = Report::factory()->make()->toArray(); 15 | 16 | $this->postJson(route('reviews.reports.store', $review), $data) 17 | ->assertCreated(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/Feature/UserTest.php: -------------------------------------------------------------------------------- 1 | group('User'); 7 | 8 | beforeEach(function () { 9 | global $user; 10 | $user = User::factory()->create(); 11 | Sanctum::actingAs($user); 12 | }); 13 | 14 | describe('Me', function () { 15 | test('user can get his info', function () { 16 | $this->getJson(route('users.me'))->assertOk(); 17 | }); 18 | }); 19 | 20 | describe('Destroy', function () { 21 | test('user can delete his account', function () { 22 | /** @var User $user */ 23 | global $user; 24 | $this->postJson(route('users.me.destroy'), [ 25 | 'username' => $user->username, 26 | ]) 27 | ->assertOk(); 28 | 29 | $this->assertModelMissing($user); 30 | }); 31 | 32 | test('user cannot delete his account with invalid username', function () { 33 | $this->postJson(route('users.me.destroy'), [ 34 | 'username' => 'random-username', 35 | ])->assertUnprocessable(); 36 | }); 37 | }); 38 | 39 | describe('Update', function () { 40 | test('user can update his username', function () { 41 | /** @var User $user */ 42 | global $user; 43 | $user->update(['username_changed_at' => now()]); 44 | $this->travel(15)->days(); 45 | 46 | $this->putJson(route('users.me.update'), [ 47 | ...$user->toArray(), 48 | 'username' => 'test.username', 49 | ]) 50 | ->assertOk() 51 | ->assertJsonPath('username', 'test.username'); 52 | 53 | $user->refresh(); 54 | expect($user->username_changed_at)->not->toBeNull(); 55 | }); 56 | 57 | test('user cannot update his username when enough time has not passed', function () { 58 | /** @var User $user */ 59 | global $user; 60 | $user->update(['username_changed_at' => now()]); 61 | 62 | $this->putJson(route('users.me.update'), [ 63 | ...$user->toArray(), 64 | 'username' => 'test.username', 65 | ])->assertUnprocessable(); 66 | }); 67 | })->skip(); 68 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | group('Architecture'); 4 | 5 | test('dd and dump should not be used') 6 | ->expect(['dd', 'dump', 'ray']) 7 | ->not->toBeUsed(); 8 | 9 | test('models should extend eloquent model and should be classes') 10 | ->expect('App\Models') 11 | ->toBeClasses() 12 | ->toExtend('Illuminate\Database\Eloquent\Model') 13 | ->ignoring('App\Models\Scopes'); 14 | 15 | test('scopes should extend scope') 16 | ->expect('App\Models\Scopes') 17 | ->toHaveSuffix('Scope') 18 | ->toImplement('Illuminate\Database\Eloquent\Scope'); 19 | 20 | test('controllers should have a suffix controller') 21 | ->expect('App\Http\Controllers') 22 | ->toHaveSuffix('Controller'); 23 | -------------------------------------------------------------------------------- /tests/Unit/DomainServiceTest.php: -------------------------------------------------------------------------------- 1 | isInExcludedDomains($domain))->toBeFalse(); 8 | })->with(['test.com', 'hello.world.org']); 9 | 10 | test('fails', function (string $domain) { 11 | expect(app(DomainService::class)->isInExcludedDomains($domain))->toBeTrue(); 12 | })->with(['username.github.io', 'sub.florisboard.org']); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/Unit/VersionNewTest.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 7 | })->with([ 8 | ['2.0.0', '1.0.0'], 9 | ['1.0.1', '1.0.0'], 10 | ['4.0.0', '3.0.2'], 11 | ['1.56.56', '1.56.55'], 12 | ['1.0.0', '0.0.1'], 13 | ]); 14 | 15 | test('failes', function (string $providedVersion, string $previousVersion) { 16 | expect(ValidateReleaseVersionName::isProvidedVersionNewer($providedVersion, $previousVersion))->toBeFalse(); 17 | })->with([ 18 | ['2.0.0', '2.0.0'], 19 | ['1.0.1', '1.0.2'], 20 | ['1.2.3', '1.3.5'], 21 | ]); 22 | --------------------------------------------------------------------------------