├── database
├── .gitignore
├── factories
│ ├── MaintainerFactory.php
│ ├── CollectionFactory.php
│ ├── ChangeProposalFactory.php
│ ├── ReportFactory.php
│ ├── MediaFactory.php
│ ├── CategoryFactory.php
│ ├── ReleaseFactory.php
│ ├── ReviewFactory.php
│ ├── DomainFactory.php
│ └── UserFactory.php
├── seeders
│ ├── CollectionSeeder.php
│ └── DatabaseSeeder.php
└── migrations
│ ├── 2024_10_14_193910_add_reviewer_description_to_projects_table.php
│ ├── 2014_10_12_100000_create_password_reset_tokens_table.php
│ ├── 2022_12_14_083707_create_settings_table.php
│ ├── 2023_11_23_125556_create_collections_table.php
│ ├── 2023_11_23_132320_create_collection_project_table.php
│ ├── 2023_11_20_125316_create_maintainers_table.php
│ ├── 2024_05_25_073616_create_domains_table.php
│ ├── 2019_08_19_000000_create_failed_jobs_table.php
│ ├── 2019_12_14_000002_create_sessions_table.php
│ ├── 2023_11_29_115411_create_reports_table.php
│ ├── 2024_06_26_200348_create_change_proposals_table.php
│ ├── 2023_11_19_205100_create_categories_table.php
│ ├── 2014_10_12_000000_create_users_table.php
│ ├── 2019_12_14_000001_create_personal_access_tokens_table.php
│ ├── 2023_11_24_124806_create_reviews_table.php
│ ├── 2023_11_23_133825_create_releases_table.php
│ ├── 2023_11_20_061313_create_media_table.php
│ └── 2023_11_19_205104_create_projects_table.php
├── resources
└── views
│ ├── .gitkeep
│ └── vendor
│ └── mail
│ ├── text
│ ├── footer.blade.php
│ ├── panel.blade.php
│ ├── subcopy.blade.php
│ ├── table.blade.php
│ ├── button.blade.php
│ ├── header.blade.php
│ ├── layout.blade.php
│ └── message.blade.php
│ └── html
│ ├── table.blade.php
│ ├── subcopy.blade.php
│ ├── footer.blade.php
│ ├── header.blade.php
│ ├── panel.blade.php
│ ├── message.blade.php
│ └── button.blade.php
├── bootstrap
├── cache
│ └── .gitignore
└── app.php
├── storage
├── logs
│ └── .gitignore
├── app
│ ├── public
│ │ └── .gitignore
│ └── .gitignore
└── framework
│ ├── testing
│ └── .gitignore
│ ├── views
│ └── .gitignore
│ ├── cache
│ ├── data
│ │ └── .gitignore
│ └── .gitignore
│ ├── sessions
│ └── .gitignore
│ └── .gitignore
├── public
├── robots.txt
├── favicon.ico
├── vendor
│ ├── telescope
│ │ ├── favicon.ico
│ │ └── mix-manifest.json
│ ├── horizon
│ │ ├── img
│ │ │ └── favicon.png
│ │ └── mix-manifest.json
│ └── log-viewer
│ │ ├── img
│ │ ├── log-viewer-128.png
│ │ ├── log-viewer-32.png
│ │ └── log-viewer-64.png
│ │ ├── mix-manifest.json
│ │ └── app.js.LICENSE.txt
├── js
│ ├── filament
│ │ └── forms
│ │ │ └── components
│ │ │ ├── textarea.js
│ │ │ ├── tags-input.js
│ │ │ └── key-value.js
│ └── novadaemon
│ │ └── filament-pretty-json
│ │ └── scripts.js
├── .htaccess
├── css
│ ├── rawilk
│ │ └── filament-password-input
│ │ │ └── filament-password-input.css
│ └── novadaemon
│ │ └── filament-pretty-json
│ │ └── styles.css
└── index.php
├── tests
├── TestCase.php
├── Feature
│ ├── AuthTest.php
│ ├── Review
│ │ └── ReviewReportTest.php
│ ├── Project
│ │ ├── ProjectReportTest.php
│ │ └── ProjectImageTest.php
│ ├── CategoryTest.php
│ ├── FileUploadTest.php
│ ├── UserTest.php
│ └── ReleaseTest.php
├── CreatesApplication.php
└── Unit
│ ├── DomainServiceTest.php
│ ├── ArchitectureTest.php
│ └── VersionNewTest.php
├── .gitattributes
├── deployment
├── utilities.sh
├── supervisord.worker.conf
├── supervisord.conf
├── supervisord.horizon.conf
├── supervisord.scheduler.conf
├── php.ini
├── octane
│ └── Swoole
│ │ └── supervisord.swoole.conf
└── start-container
├── app
├── Models
│ ├── Media.php
│ ├── Scopes
│ │ └── ActiveScope.php
│ ├── Domain.php
│ ├── Maintainer.php
│ ├── Collection.php
│ ├── Category.php
│ ├── ChangeProposal.php
│ ├── Report.php
│ ├── Review.php
│ └── Release.php
├── Filament
│ ├── Resources
│ │ ├── UserResource
│ │ │ ├── Pages
│ │ │ │ ├── CreateUser.php
│ │ │ │ ├── EditUser.php
│ │ │ │ └── ListUsers.php
│ │ │ └── RelationManagers
│ │ │ │ ├── DomainsRelationManager.php
│ │ │ │ ├── ReviewsRelationManager.php
│ │ │ │ ├── ProjectsRelationManager.php
│ │ │ │ ├── ReleasesRelationManager.php
│ │ │ │ ├── CollectionsRelationManager.php
│ │ │ │ └── MaintainingRelationManager.php
│ │ ├── DomainResource
│ │ │ └── Pages
│ │ │ │ ├── CreateDomain.php
│ │ │ │ ├── EditDomain.php
│ │ │ │ └── ListDomains.php
│ │ ├── ReportResource
│ │ │ └── Pages
│ │ │ │ ├── CreateReport.php
│ │ │ │ ├── EditReport.php
│ │ │ │ └── ListReports.php
│ │ ├── ReviewResource
│ │ │ ├── Pages
│ │ │ │ ├── CreateReview.php
│ │ │ │ ├── ListReviews.php
│ │ │ │ └── EditReview.php
│ │ │ └── RelationManagers
│ │ │ │ └── ReportsRelationManager.php
│ │ ├── ProjectResource
│ │ │ ├── Pages
│ │ │ │ ├── CreateProject.php
│ │ │ │ ├── ListProjects.php
│ │ │ │ └── EditProject.php
│ │ │ └── RelationManagers
│ │ │ │ ├── ReportsRelationManager.php
│ │ │ │ ├── ReviewsRelationManager.php
│ │ │ │ ├── ReleasesRelationManager.php
│ │ │ │ ├── ChangeProposalsRelationManager.php
│ │ │ │ └── CollectionsRelationManager.php
│ │ ├── ReleaseResource
│ │ │ └── Pages
│ │ │ │ ├── CreateRelease.php
│ │ │ │ ├── EditRelease.php
│ │ │ │ └── ListReleases.php
│ │ ├── CategoryResource
│ │ │ ├── Pages
│ │ │ │ ├── CreateCategory.php
│ │ │ │ ├── EditCategory.php
│ │ │ │ └── ListCategories.php
│ │ │ └── RelationManagers
│ │ │ │ └── ProjectsRelationManager.php
│ │ ├── CollectionResource
│ │ │ ├── Pages
│ │ │ │ ├── CreateCollection.php
│ │ │ │ ├── EditCollection.php
│ │ │ │ └── ListCollections.php
│ │ │ └── RelationManagers
│ │ │ │ └── ProjectsRelationManager.php
│ │ └── ChangeProposalResource
│ │ │ └── Pages
│ │ │ ├── CreateChangeProposal.php
│ │ │ └── ListChangeProposals.php
│ ├── Forms
│ │ ├── Components
│ │ │ ├── ImageInput.php
│ │ │ └── FileInput.php
│ │ └── Layouts
│ │ │ ├── SideGroup.php
│ │ │ ├── MainGroup.php
│ │ │ ├── BasicSection.php
│ │ │ ├── ImagesSection.php
│ │ │ ├── BasicForm.php
│ │ │ ├── ComplexForm.php
│ │ │ ├── TimestampsSection.php
│ │ │ └── StatusSection.php
│ ├── Custom
│ │ ├── CustomResource.php
│ │ └── AutoRelationManager.php
│ └── Tables
│ │ └── Components
│ │ └── TimestampsColumn.php
├── Http
│ ├── Controllers
│ │ ├── Controller.php
│ │ ├── LoginAsController.php
│ │ ├── Auth
│ │ │ ├── AuthController.php
│ │ │ └── GithubController.php
│ │ ├── FileUploadController.php
│ │ ├── Domain
│ │ │ └── DomainVerifyController.php
│ │ ├── HomeController.php
│ │ ├── Review
│ │ │ └── ReviewReportController.php
│ │ ├── Project
│ │ │ ├── ProjectReportController.php
│ │ │ ├── ProjectImageController.php
│ │ │ └── ScreenshotController.php
│ │ ├── CategoryController.php
│ │ ├── CheckUpdateController.php
│ │ └── CollectionController.php
│ ├── Middleware
│ │ ├── EncryptCookies.php
│ │ ├── VerifyCsrfToken.php
│ │ ├── PreventRequestsDuringMaintenance.php
│ │ ├── TrimStrings.php
│ │ ├── TrustHosts.php
│ │ ├── Authenticate.php
│ │ ├── ValidateSignature.php
│ │ ├── TrustProxies.php
│ │ ├── EnsureEmailIsVerified.php
│ │ └── RedirectIfAuthenticated.php
│ ├── Resources
│ │ ├── User
│ │ │ ├── UserResource.php
│ │ │ └── AuthResource.php
│ │ ├── Media
│ │ │ └── ImageResource.php
│ │ ├── Project
│ │ │ ├── ProjectSlimResource.php
│ │ │ └── ProjectResource.php
│ │ ├── CategoryResource.php
│ │ ├── CheckUpdateResource.php
│ │ ├── Release
│ │ │ ├── ReleaseResource.php
│ │ │ └── ReleaseFullResource.php
│ │ ├── DomainResource.php
│ │ ├── CollectionResource.php
│ │ ├── ReviewResource.php
│ │ └── ChangeProposalResource.php
│ └── Requests
│ │ ├── ReviewRequest.php
│ │ ├── StoreReportRequest.php
│ │ ├── Release
│ │ └── StoreReleaseRequest.php
│ │ └── Project
│ │ └── UpdateProjectRequest.php
├── Providers
│ ├── BroadcastServiceProvider.php
│ ├── AuthServiceProvider.php
│ ├── HorizonServiceProvider.php
│ ├── EventServiceProvider.php
│ ├── RouteServiceProvider.php
│ ├── TelescopeServiceProvider.php
│ ├── AppServiceProvider.php
│ └── Filament
│ │ └── AdminPanelProvider.php
├── Enums
│ ├── AuthProviderEnum.php
│ ├── ReportTypeEnum.php
│ ├── ProjectTypeEnum.php
│ └── StatusEnum.php
├── Rules
│ ├── Username.php
│ └── FileUpload.php
├── Services
│ ├── UserService.php
│ ├── CategoryService.php
│ ├── ReleaseService.php
│ └── DomainService.php
├── Exceptions
│ └── Handler.php
├── Console
│ ├── Commands
│ │ ├── DeleteUnverifiedDomains.php
│ │ └── DeleteTempUploadedFiles.php
│ └── Kernel.php
├── Observers
│ └── UserObserver.php
├── Validations
│ └── ValidateReleaseVersionName.php
└── Policies
│ └── ReviewPolicy.php
├── .gitignore
├── .editorconfig
├── phpstan.neon
├── routes
├── web.php
├── channels.php
├── console.php
└── auth.php
├── .dockerignore
├── config
├── cors.php
├── view.php
├── services.php
├── query-builder.php
├── hashing.php
└── scramble.php
├── phpunit.xml
├── artisan
├── .env.example
└── docker-compose.prod.yml
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite*
2 |
--------------------------------------------------------------------------------
/resources/views/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/bootstrap/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/storage/app/public/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/app/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !public/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/storage/framework/testing/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/views/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/cache/data/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/sessions/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/text/footer.blade.php:
--------------------------------------------------------------------------------
1 | {{ $slot }}
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/storage/framework/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !data/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/text/button.blade.php:
--------------------------------------------------------------------------------
1 | {{ $slot }}: {{ $url }}
2 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/text/header.blade.php:
--------------------------------------------------------------------------------
1 | [{{ $slot }}]({{ $url }})
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/florisboard/addons-backend/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/vendor/telescope/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/florisboard/addons-backend/HEAD/public/vendor/telescope/favicon.ico
--------------------------------------------------------------------------------
/resources/views/vendor/mail/html/table.blade.php:
--------------------------------------------------------------------------------
1 |
2 | {{ Illuminate\Mail\Markdown::parse($slot) }}
3 |
4 |
--------------------------------------------------------------------------------
/public/vendor/horizon/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/florisboard/addons-backend/HEAD/public/vendor/horizon/img/favicon.png
--------------------------------------------------------------------------------
/public/vendor/log-viewer/img/log-viewer-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/florisboard/addons-backend/HEAD/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/HEAD/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/HEAD/public/vendor/log-viewer/img/log-viewer-64.png
--------------------------------------------------------------------------------
/storage/framework/.gitignore:
--------------------------------------------------------------------------------
1 | compiled.php
2 | config.php
3 | down
4 | events.scanned.php
5 | maintenance.php
6 | routes.php
7 | routes.scanned.php
8 | schedule-*
9 | services.json
10 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | |
4 | {{ Illuminate\Mail\Markdown::parse($slot) }}
5 | |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/Feature/AuthTest.php:
--------------------------------------------------------------------------------
1 | actingAs(User::factory()->create())
7 | ->post(route('logout'))
8 | ->assertNoContent();
9 |
10 | $this->assertGuest();
11 | });
12 |
--------------------------------------------------------------------------------
/app/Models/Media.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 | |
11 |
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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/app/Filament/Resources/UserResource/Pages/CreateUser.php:
--------------------------------------------------------------------------------
1 |
3 |
12 |
13 |
--------------------------------------------------------------------------------
/app/Filament/Resources/DomainResource/Pages/CreateDomain.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | |
7 | {{ Illuminate\Mail\Markdown::parse($slot) }}
8 | |
9 |
10 |
11 | |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/Http/Middleware/EncryptCookies.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [
15 | //
16 | ];
17 | }
18 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/app/Filament/Forms/Components/ImageInput.php:
--------------------------------------------------------------------------------
1 | image()
13 | ->imageEditor();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/Http/Middleware/VerifyCsrfToken.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [
15 | //
16 | ];
17 | }
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/vendor/log-viewer/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/app.js": "/app.js?id=74d3e481ad3e8fa14f1daaa0aa46e109",
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 |
--------------------------------------------------------------------------------
/app/Filament/Resources/UserResource/RelationManagers/DomainsRelationManager.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/Resources/ProjectResource/RelationManagers/ReleasesRelationManager.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/Resources/CategoryResource/RelationManagers/ProjectsRelationManager.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | public static function getEloquentQuery(): Builder
15 | {
16 | return parent::getEloquentQuery()->withoutGlobalScopes();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/Filament/Resources/UserResource/RelationManagers/CollectionsRelationManager.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [
15 | //
16 | ];
17 | }
18 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [
15 | 'current_password',
16 | 'password',
17 | 'password_confirmation',
18 | ];
19 | }
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/CreatesApplication.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class)->bootstrap();
18 |
19 | return $app;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrustHosts.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | public function hosts(): array
15 | {
16 | return [
17 | $this->allSubdomainsOfApplicationUrl(),
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/Filament/Resources/ProjectResource/RelationManagers/ChangeProposalsRelationManager.php:
--------------------------------------------------------------------------------
1 | expectsJson() ? null : route('login');
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/Filament/Resources/UserResource/Pages/EditUser.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Filament/Resources/DomainResource/Pages/EditDomain.php:
--------------------------------------------------------------------------------
1 | $builder
15 | */
16 | public function apply(Builder $builder, Model $model): void
17 | {
18 | $builder->where('is_active', true);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Filament/Resources/CollectionResource/Pages/EditCollection.php:
--------------------------------------------------------------------------------
1 | sortable()->dateTime()->toggleable(),
16 | Tables\Columns\TextColumn::make('updated_at')->sortable()->dateTime()->toggleable(),
17 | ];
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/Filament/Resources/CollectionResource/Pages/ListCollections.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Enums/AuthProviderEnum.php:
--------------------------------------------------------------------------------
1 | name)->ucsplit()->join(' ');
19 | }
20 |
21 | public function getColor(): string
22 | {
23 | return 'primary';
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/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/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/Rules/Username.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/Resources/ReviewResource/Pages/EditReview.php:
--------------------------------------------------------------------------------
1 | id === (int) $id;
18 | });
19 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/routes/console.php:
--------------------------------------------------------------------------------
1 | comment(Inspiring::quote());
19 | })->purpose('Display an inspiring quote');
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Services/CategoryService.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/html/button.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'url',
3 | 'color' => 'primary',
4 | 'align' => 'center',
5 | ])
6 |
7 |
8 |
9 |
10 |
11 | |
12 |
19 | |
20 |
21 |
22 | |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/Unit/ArchitectureTest.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/Enums/ReportTypeEnum.php:
--------------------------------------------------------------------------------
1 | name)->ucsplit()->join(' ');
24 | }
25 |
26 | public function getColor(): string
27 | {
28 | return 'primary';
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/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))}
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Console/Commands/DeleteUnverifiedDomains.php:
--------------------------------------------------------------------------------
1 | whereNull('verified_at')
31 | ->where('created_at', '<', now()->subDay())
32 | ->delete();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/Middleware/RedirectIfAuthenticated.php:
--------------------------------------------------------------------------------
1 | check()) {
24 | return redirect(RouteServiceProvider::HOME);
25 | }
26 | }
27 |
28 | return $next($request);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/Filament/Resources/CollectionResource/RelationManagers/ProjectsRelationManager.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Filament/Resources/ProjectResource/RelationManagers/CollectionsRelationManager.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/app/Providers/HorizonServiceProvider.php:
--------------------------------------------------------------------------------
1 | isAdministrator();
33 | });
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Domain/DomainVerifyController.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Providers/EventServiceProvider.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Filament/Resources/UserResource/RelationManagers/MaintainingRelationManager.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 |
--------------------------------------------------------------------------------
/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.+-]*|\[\])?([,[{])?$/mg;
10 | var replacer = function (match, pIndent, pKey, pKeyContent, 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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/Console/Commands/DeleteTempUploadedFiles.php:
--------------------------------------------------------------------------------
1 | info("Checking $file");
35 | if (now()->diffInDays($lastModified) > 20) {
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/Models/Release.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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/artisan:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | make(Illuminate\Contracts\Console\Kernel::class);
34 |
35 | $status = $kernel->handle(
36 | $input = new Symfony\Component\Console\Input\ArgvInput,
37 | new Symfony\Component\Console\Output\ConsoleOutput
38 | );
39 |
40 | /*
41 | |--------------------------------------------------------------------------
42 | | Shutdown The Application
43 | |--------------------------------------------------------------------------
44 | |
45 | | Once Artisan has finished running, we will fire off the shutdown events
46 | | so that any final work may be done by the application before we shut
47 | | down the process. This is the last thing to happen to the request.
48 | |
49 | */
50 |
51 | $kernel->terminate($input, $status);
52 |
53 | exit($status);
54 |
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class);
50 |
51 | $response = $kernel->handle(
52 | $request = Request::capture()
53 | )->send();
54 |
55 | $kernel->terminate($request, $response);
56 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------