mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-13 04:30:41 -05:00
Merge branch 'develop'
This commit is contained in:
commit
9ab747b19e
10
.env.ci
10
.env.ci
@ -11,13 +11,13 @@ LOG_LEVEL=debug
|
|||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_PORT=33306
|
DB_PORT=33306
|
||||||
DB_DATABASE=test
|
DB_DATABASE=testing
|
||||||
DB_USERNAME=root
|
DB_USERNAME=user
|
||||||
DB_PASSWORD=
|
DB_PASSWORD=password
|
||||||
|
|
||||||
BROADCAST_DRIVER=log
|
SCOUT_DRIVER=null
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
QUEUE_CONNECTION=sync
|
QUEUE_CONNECTION=sync
|
||||||
CACHE_DRIVER=array
|
CACHE_STORE=array
|
||||||
EMAIL_DRIVER=array
|
EMAIL_DRIVER=array
|
||||||
SESSION_DRIVER=array
|
SESSION_DRIVER=array
|
||||||
|
37
.env.example
37
.env.example
@ -3,7 +3,7 @@ APP_ENV=local
|
|||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_TIMEZONE=UTC
|
APP_TIMEZONE=UTC
|
||||||
APP_URL=http://forge.test
|
APP_URL=http://localhost
|
||||||
APP_LOCALE=en
|
APP_LOCALE=en
|
||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
APP_FAKER_LOCALE=en_US
|
APP_FAKER_LOCALE=en_US
|
||||||
@ -18,21 +18,22 @@ LOG_STACK=single
|
|||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# Due to the hub import script, only MySQL is supported at this time.
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=localhost
|
DB_HOST=mysql
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=forge
|
DB_DATABASE=forge
|
||||||
DB_USERNAME=root
|
DB_USERNAME=forge
|
||||||
DB_PASSWORD=
|
DB_PASSWORD=password
|
||||||
|
|
||||||
# This is only needed if you are running the app:import-hub command.
|
# This is only needed if you are running the app:import-hub command.
|
||||||
# For normal development you should just seed the database with fake data:
|
# For normal development you should just seed the database with fake data:
|
||||||
# `php artisan migrate:fresh --seed`
|
# `php artisan migrate:fresh --seed`
|
||||||
DB_HUB_CONNECTION=mysql
|
DB_HUB_CONNECTION=mysql
|
||||||
DB_HUB_HOST=localhost
|
DB_HUB_HOST=
|
||||||
DB_HUB_PORT=3306
|
DB_HUB_PORT=
|
||||||
DB_HUB_DATABASE=forge
|
DB_HUB_DATABASE=
|
||||||
DB_HUB_USERNAME=root
|
DB_HUB_USERNAME=
|
||||||
DB_HUB_PASSWORD=
|
DB_HUB_PASSWORD=
|
||||||
|
|
||||||
SESSION_DRIVER=redis
|
SESSION_DRIVER=redis
|
||||||
@ -43,32 +44,32 @@ SESSION_ENCRYPT=false
|
|||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
SESSION_DOMAIN=null
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
|
ASSET_URL=http://localhost/storage
|
||||||
|
|
||||||
CACHE_STORE=redis
|
CACHE_STORE=redis
|
||||||
QUEUE_CONNECTION=redis
|
QUEUE_CONNECTION=redis
|
||||||
|
|
||||||
REDIS_HOST=127.0.0.1
|
REDIS_HOST=redis
|
||||||
REDIS_PASSWORD=null
|
REDIS_PASSWORD=null
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
REDIS_QUEUE=default
|
||||||
REDIS_CACHE_CONNECTION=cache
|
REDIS_CACHE_CONNECTION=cache
|
||||||
REDIS_QUEUE_CONNECTION=queue
|
|
||||||
REDIS_QUEUE=queue
|
|
||||||
|
|
||||||
SCOUT_DRIVER=meilisearch
|
|
||||||
SCOUT_QUEUE=true
|
SCOUT_QUEUE=true
|
||||||
|
SCOUT_DRIVER=meilisearch
|
||||||
MEILISEARCH_HOST=http://127.0.0.1:7700
|
MEILISEARCH_HOST=http://meilisearch:7700
|
||||||
MEILISEARCH_KEY=LARAVEL-HERD
|
MEILISEARCH_KEY=
|
||||||
MEILISEARCH_NO_ANALYTICS=true
|
MEILISEARCH_NO_ANALYTICS=true
|
||||||
|
|
||||||
MAIL_MAILER=smtp
|
MAIL_MAILER=smtp
|
||||||
MAIL_HOST=127.0.0.1
|
MAIL_HOST=mailpit
|
||||||
MAIL_PORT=2525
|
MAIL_PORT=1025
|
||||||
MAIL_USERNAME=${APP_NAME}
|
MAIL_USERNAME=${APP_NAME}
|
||||||
MAIL_ENCRYPTION=null
|
MAIL_ENCRYPTION=null
|
||||||
MAIL_FROM_ADDRESS="no-reply@sp-tarkov.com"
|
MAIL_FROM_ADDRESS="no-reply@sp-tarkov.com"
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
NOVA_LICENSE_KEY=
|
NOVA_LICENSE_KEY=
|
||||||
|
|
||||||
|
SAIL_XDEBUG_MODE=develop,debug,coverage
|
||||||
|
97
.github/README.md
vendored
Normal file
97
.github/README.md
vendored
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<p align="center"><a href="https://forge.sp-tarkov.com" target="_blank"><img src="logo.spt.png" width="400" alt="Single Player Tarkov Logo"></a></p>
|
||||||
|
<h1 align="center"><em>The Forge</em></h1>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.mozilla.org/en-US/MPL/2.0/"><img src="https://img.shields.io/badge/License-MPL_2.0-blue.svg" alt="License: MPL 2.0"></a>
|
||||||
|
<a href="https://github.com/sp-tarkov/forge/actions/workflows/quality.yaml"><img src="https://github.com/sp-tarkov/forge/actions/workflows/quality.yaml/badge.svg" alt="Quality Control Action Status"></a>
|
||||||
|
<a href="https://github.com/sp-tarkov/forge/actions/workflows/tests.yaml"><img src="https://github.com/sp-tarkov/forge/actions/workflows/tests.yaml/badge.svg" alt="Test Action Status"></a>
|
||||||
|
<a href="https://discord.com/invite/Xn9msqQZan"><img src="https://img.shields.io/badge/Chat-Discord-5865F2?logo=discord&logoColor=ffffff" alt="Discord Chat"></a>
|
||||||
|
<a href="https://www.patreon.com/sptarkov"><img src="https://img.shields.io/badge/Fund-Patreon-fe3c71?logo=patreon&logoColor=ffffff" alt="Patreon Fund"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
The Forge is a Laravel-based web application that provides a platform for the Single Player Tarkov community to share and discover user-generated content, such as mods, guides, and other tools. It is currently under heavy development. Please review this entire document before attempting to contribute, especially the "Development Discussion" section.
|
||||||
|
|
||||||
|
## Development Environment Setup
|
||||||
|
|
||||||
|
This is a [Laravel](https://laravel.com/docs/11.x) project that uses [Sail](https://laravel.com/docs/11.x/sail), which provides a Docker-based development environment. Ensure you review the Sail documentation for useage, particularly in a [Windows environment](https://laravel.com/docs/11.x/installation#sail-on-windows), as WSL2 is recommended.
|
||||||
|
|
||||||
|
### Accessing the Application:
|
||||||
|
|
||||||
|
Once the Docker containers are running with Sail you can access the application at <http://localhost>.
|
||||||
|
|
||||||
|
### Available Services:
|
||||||
|
|
||||||
|
| Service | Access Via Application | Access Via Host |
|
||||||
|
|-------------|------------------------|------------------|
|
||||||
|
| MySQL | `mysql:3306` | `localhost:3306` |
|
||||||
|
| Redis | `redis:6379` | `localhost:6379` |
|
||||||
|
| Meilisearch | `meilisearch:7700` | `localhost:7700` |
|
||||||
|
| Mailpit | `mailpit:1025` | `localhost:8025` |
|
||||||
|
|
||||||
|
### Notable Routes
|
||||||
|
|
||||||
|
| Service | Authentication | Access Via Host |
|
||||||
|
|----------------------------------|----------------|----------------------------|
|
||||||
|
| Administration Panel (Nova) | Via User Role | <http://localhost/nova> |
|
||||||
|
| Redis Queue Management (Horizon) | Via User Role | <http://localhost/horizon> |
|
||||||
|
| Website Status (Pulse) | Via User Role | <http://localhost/pulse> |
|
||||||
|
| Meilisearch WebUI | Local Only | <http://localhost:7700> |
|
||||||
|
| Mailpit WebUI | Local Only | <http://localhost:8025> |
|
||||||
|
|
||||||
|
Most of these connection settings should already be configured in the `.env.example` file. Simply save the `.env.example` file as `.env` and adjust further settings as needed.
|
||||||
|
|
||||||
|
### Basic Usage Examples
|
||||||
|
|
||||||
|
Here are some basic commands to get started with Forge:
|
||||||
|
|
||||||
|
```
|
||||||
|
# View all of the available Artisan commands:
|
||||||
|
./vendor/bin/sail artisan
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
# Migrate and seed the database with test data:
|
||||||
|
./vendor/bin/sail artisan migrate:fresh –seed
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
# Run Laravel Horizon (the queue monitor):
|
||||||
|
./vendor/bin/sail artisan horizon
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
# Start the local
|
||||||
|
./vendor/bin/sail artisan horizon
|
||||||
|
```
|
||||||
|
|
||||||
|
### More Information
|
||||||
|
|
||||||
|
For more information on Laravel development, please refer to the [official documentation](https://laravel.com/docs/11.x/).
|
||||||
|
|
||||||
|
## Development Discussion
|
||||||
|
|
||||||
|
*__Please note__, we are very early in development and will likely not accept work that is not discussed beforehand through the following channels...*
|
||||||
|
|
||||||
|
You may propose new features or improvements of existing Forge behavior in [the repository's GitHub discussion board](https://github.com/sp-tarkov/forge/discussions). If you propose a new feature, please be willing to implement at least some of the code that would be needed to complete the feature.
|
||||||
|
|
||||||
|
Informal discussion regarding bugs, new features, and implementation of existing features takes place in the `#website-general` channel of the [Single Player Tarkov Discord server](https://discord.com/invite/Xn9msqQZan). Refringe, the maintainer of Forge, is typically present in the channel on weekdays from 9am-5pm Eastern Time (ET), and sporadically present in the channel at other times.
|
||||||
|
|
||||||
|
## Which Branch?
|
||||||
|
|
||||||
|
The `main` branch is the default branch for Forge. This branch is used for the latest stable release of the site. The `develop` branch is used for the latest development changes. All feature branches should be based on the `develop` branch. All pull requests should target the `develop` branch.
|
||||||
|
|
||||||
|
## Security Vulnerabilities
|
||||||
|
|
||||||
|
If you discover a security vulnerability within Forge, please email Refringe at me@refringe.com. All security vulnerabilities will be promptly addressed.
|
||||||
|
|
||||||
|
## Coding Style
|
||||||
|
|
||||||
|
Forge follows the PSR-2 coding standard and the PSR-4 autoloading standard. We use an automated Laravel Pint action to enforce the coding standard, though it's suggested to run your code changes through Pint before contributing.
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
The Forge development code of conduct is derived from the Ruby code of conduct. Any violations of the code of conduct may be reported to Refringe at me@refringe.com.
|
||||||
|
|
||||||
|
- Participants will be tolerant of opposing views.
|
||||||
|
- Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
||||||
|
- When interpreting the words and actions of others, participants should always assume good intentions.
|
||||||
|
- Behavior that can be reasonably considered harassment will not be tolerated.
|
BIN
.github/logo.spt.png
vendored
Normal file
BIN
.github/logo.spt.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
65
.github/workflows/larastan.yml
vendored
65
.github/workflows/larastan.yml
vendored
@ -1,65 +0,0 @@
|
|||||||
name: Larastan Static Analysis
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches-ignore:
|
|
||||||
- 'dependabot/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
larastan:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.3'
|
|
||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
|
||||||
coverage: none
|
|
||||||
|
|
||||||
- name: Configure Laravel Nova Authentication
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
|
|
||||||
NOVA_LICENSE_KEY: ${{ secrets.NOVA_LICENSE_KEY }}
|
|
||||||
run: |
|
|
||||||
composer config http-basic.nova.laravel.com "$NOVA_USERNAME" "$NOVA_LICENSE_KEY"
|
|
||||||
|
|
||||||
- name: Get Composer Cache Directory
|
|
||||||
id: composer-cache
|
|
||||||
run: |
|
|
||||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
id: actions-cache
|
|
||||||
with:
|
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-composer-
|
|
||||||
|
|
||||||
- name: Cache PHP dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
id: vendor-cache
|
|
||||||
with:
|
|
||||||
path: vendor
|
|
||||||
key: ${{ runner.OS }}-build-${{ hashFiles('**/composer.lock') }}
|
|
||||||
|
|
||||||
- name: Copy .env
|
|
||||||
run: php -r "file_exists('.env') || copy('.env.ci', '.env');"
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
if: steps.vendor-cache.outputs.cache-hit != 'true'
|
|
||||||
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
|
||||||
|
|
||||||
- name: Update Dependencies with latest stable
|
|
||||||
run: composer update --prefer-stable
|
|
||||||
|
|
||||||
- name: Execute Code Static Analysis with Larastan
|
|
||||||
run: |
|
|
||||||
composer require --dev larastan/larastan
|
|
||||||
./vendor/bin/phpstan analyse -c ./phpstan.neon --no-progress
|
|
73
.github/workflows/pint.yml
vendored
73
.github/workflows/pint.yml
vendored
@ -1,73 +0,0 @@
|
|||||||
name: Pint Code Style Fixer
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches-ignore:
|
|
||||||
- 'dependabot/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pint-fixer:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.head_ref }}
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.3'
|
|
||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
|
||||||
coverage: none
|
|
||||||
|
|
||||||
- name: Configure Laravel Nova Authentication
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
|
|
||||||
NOVA_LICENSE_KEY: ${{ secrets.NOVA_LICENSE_KEY }}
|
|
||||||
run: |
|
|
||||||
composer config http-basic.nova.laravel.com "$NOVA_USERNAME" "$NOVA_LICENSE_KEY"
|
|
||||||
|
|
||||||
- name: Get Composer Cache Directory
|
|
||||||
id: composer-cache
|
|
||||||
run: |
|
|
||||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
id: actions-cache
|
|
||||||
with:
|
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-composer-
|
|
||||||
|
|
||||||
- name: Cache PHP dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
id: vendor-cache
|
|
||||||
with:
|
|
||||||
path: vendor
|
|
||||||
key: ${{ runner.OS }}-build-${{ hashFiles('**/composer.lock') }}
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
if: steps.vendor-cache.outputs.cache-hit != 'true'
|
|
||||||
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
|
||||||
|
|
||||||
- name: Update Dependencies with latest stable
|
|
||||||
run: composer update --prefer-stable
|
|
||||||
|
|
||||||
- name: Run Pint Code Style Fixer
|
|
||||||
run: |
|
|
||||||
composer require laravel/pint --dev
|
|
||||||
./vendor/bin/pint
|
|
||||||
|
|
||||||
- uses: stefanzweifel/git-auto-commit-action@v5
|
|
||||||
with:
|
|
||||||
commit_message: Pint PHP Style Fixes
|
|
||||||
commit_user_name: Pint Bot
|
|
||||||
skip_fetch: true
|
|
||||||
file_pattern: '*.php'
|
|
98
.github/workflows/quality.yaml
vendored
Normal file
98
.github/workflows/quality.yaml
vendored
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
name: Quality
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security-checker:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: The PHP Security Checker
|
||||||
|
uses: symfonycorp/security-checker-action@v5
|
||||||
|
|
||||||
|
larastan:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: mbstring, dom, fileinfo
|
||||||
|
coverage: none
|
||||||
|
- name: Configure Laravel Nova Authentication
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
|
||||||
|
NOVA_LICENSE_KEY: ${{ secrets.NOVA_LICENSE_KEY }}
|
||||||
|
run: composer config http-basic.nova.laravel.com "$NOVA_USERNAME" "$NOVA_LICENSE_KEY"
|
||||||
|
- name: Get Composer Cache Directory
|
||||||
|
id: composer-cache
|
||||||
|
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||||
|
- name: Cache composer dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: ${{ runner.os }}-composer-
|
||||||
|
- name: Install Composer Dependencies
|
||||||
|
run: composer install --no-progress --prefer-dist --optimize-autoloader
|
||||||
|
- name: Prepare Laravel Environment
|
||||||
|
run: |
|
||||||
|
php -r "file_exists('.env') || copy('.env.ci', '.env');"
|
||||||
|
php artisan key:generate
|
||||||
|
php artisan config:cache
|
||||||
|
php artisan route:cache
|
||||||
|
- name: Clear Laravel Config
|
||||||
|
run: php artisan config:clear
|
||||||
|
- name: Execute Code Static Analysis with Larastan
|
||||||
|
run: ./vendor/bin/phpstan analyse -c ./phpstan.neon --no-progress --error-format=github
|
||||||
|
|
||||||
|
pint-fixer:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: mbstring, dom, fileinfo
|
||||||
|
coverage: none
|
||||||
|
- name: Configure Laravel Nova Authentication
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
|
||||||
|
NOVA_LICENSE_KEY: ${{ secrets.NOVA_LICENSE_KEY }}
|
||||||
|
run: composer config http-basic.nova.laravel.com "$NOVA_USERNAME" "$NOVA_LICENSE_KEY"
|
||||||
|
- name: Get Composer Cache Directory
|
||||||
|
id: composer-cache
|
||||||
|
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||||
|
- name: Cache composer dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: ${{ runner.os }}-composer-
|
||||||
|
- name: Install Composer Dependencies
|
||||||
|
run: composer install --no-progress --prefer-dist --optimize-autoloader
|
||||||
|
- name: Prepare Laravel Environment
|
||||||
|
run: |
|
||||||
|
php -r "file_exists('.env') || copy('.env.ci', '.env');"
|
||||||
|
php artisan key:generate
|
||||||
|
php artisan config:cache
|
||||||
|
php artisan route:cache
|
||||||
|
- name: Clear Laravel Config
|
||||||
|
run: php artisan config:clear
|
||||||
|
- name: Run Pint Code Style Fixer
|
||||||
|
run: ./vendor/bin/pint
|
||||||
|
- uses: stefanzweifel/git-auto-commit-action@v5
|
||||||
|
with:
|
||||||
|
commit_message: Pint PHP Style Fixes [no ci]
|
||||||
|
commit_user_name: Pint Bot
|
||||||
|
skip_fetch: true
|
||||||
|
file_pattern: '*.php'
|
16
.github/workflows/security.yml
vendored
16
.github/workflows/security.yml
vendored
@ -1,16 +0,0 @@
|
|||||||
name: Security Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
security-check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Security Checker
|
|
||||||
uses: symfonycorp/security-checker-action@v5
|
|
72
.github/workflows/tests.yaml
vendored
Normal file
72
.github/workflows/tests.yaml
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
laravel-tests:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.3
|
||||||
|
env:
|
||||||
|
MYSQL_DATABASE: testing
|
||||||
|
MYSQL_USER: user
|
||||||
|
MYSQL_PASSWORD: password
|
||||||
|
MYSQL_ROOT_PASSWORD: password
|
||||||
|
ports:
|
||||||
|
- 33306:3306
|
||||||
|
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: mbstring, dom, fileinfo
|
||||||
|
coverage: none
|
||||||
|
- name: Configure Laravel Nova Authentication
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
|
||||||
|
NOVA_LICENSE_KEY: ${{ secrets.NOVA_LICENSE_KEY }}
|
||||||
|
run: composer config http-basic.nova.laravel.com "$NOVA_USERNAME" "$NOVA_LICENSE_KEY"
|
||||||
|
- name: Get Composer Cache Directory
|
||||||
|
id: composer-cache
|
||||||
|
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||||
|
- name: Cache composer dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: ${{ runner.os }}-composer-
|
||||||
|
- name: Install Composer Dependencies
|
||||||
|
run: composer install --no-progress --prefer-dist --optimize-autoloader
|
||||||
|
- name: Get NPM Cache Directory
|
||||||
|
id: npm-cache
|
||||||
|
run: echo "NPM_CACHE_DIR=$(npm config get cache)" >> $GITHUB_ENV
|
||||||
|
- name: Cache NPM Dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.NPM_CACHE_DIR }}
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: ${{ runner.os }}-node-
|
||||||
|
- name: Install npm dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Build Front-end Assets
|
||||||
|
run: npm run build
|
||||||
|
- name: Prepare Laravel Environment
|
||||||
|
run: |
|
||||||
|
php -r "file_exists('.env') || copy('.env.ci', '.env');"
|
||||||
|
php artisan key:generate
|
||||||
|
php artisan config:cache
|
||||||
|
php artisan route:cache
|
||||||
|
- name: Run Database Migrations
|
||||||
|
run: php artisan migrate
|
||||||
|
- name: Link Storage
|
||||||
|
run: php artisan storage:link
|
||||||
|
- name: Run Tests
|
||||||
|
run: php artisan test
|
||||||
|
- name: Display Laravel Log
|
||||||
|
if: failure()
|
||||||
|
run: cat storage/logs/laravel.log
|
118
.github/workflows/tests.yml
vendored
118
.github/workflows/tests.yml
vendored
@ -1,118 +0,0 @@
|
|||||||
name: Laravel Pest Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- develop
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
laravel-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
services:
|
|
||||||
mysql:
|
|
||||||
image: mysql:8.3
|
|
||||||
env:
|
|
||||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
|
||||||
MYSQL_DATABASE: test
|
|
||||||
ports:
|
|
||||||
- 33306:3306
|
|
||||||
options: >-
|
|
||||||
--health-cmd="mysqladmin ping"
|
|
||||||
--health-interval=10s
|
|
||||||
--health-timeout=5s
|
|
||||||
--health-retries=3
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Cache node_modules Directory
|
|
||||||
uses: actions/cache@v4
|
|
||||||
id: node_modules-cache
|
|
||||||
with:
|
|
||||||
path: node_modules
|
|
||||||
key: ${{ runner.OS }}-build-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
|
|
||||||
- name: Install NPM Packages
|
|
||||||
if: steps.node_modules-cache.outputs.cache-hit != 'true'
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build Frontend
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.3'
|
|
||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
|
|
||||||
coverage: none
|
|
||||||
|
|
||||||
- name: Configure Laravel Nova Authentication
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
|
|
||||||
NOVA_LICENSE_KEY: ${{ secrets.NOVA_LICENSE_KEY }}
|
|
||||||
run: |
|
|
||||||
composer config http-basic.nova.laravel.com "$NOVA_USERNAME" "$NOVA_LICENSE_KEY"
|
|
||||||
|
|
||||||
- name: Get Composer Cache Directory
|
|
||||||
id: composer-cache
|
|
||||||
run: |
|
|
||||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
id: actions-cache
|
|
||||||
with:
|
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-composer-
|
|
||||||
|
|
||||||
- name: Cache PHP dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
id: vendor-cache
|
|
||||||
with:
|
|
||||||
path: vendor
|
|
||||||
key: ${{ runner.OS }}-build-${{ hashFiles('**/composer.lock') }}
|
|
||||||
|
|
||||||
- name: Copy .env
|
|
||||||
run: php -r "file_exists('.env') || copy('.env.ci', '.env');"
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
if: steps.vendor-cache.outputs.cache-hit != 'true'
|
|
||||||
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
|
||||||
|
|
||||||
- name: Update Dependencies with latest stable
|
|
||||||
run: composer update --prefer-stable
|
|
||||||
|
|
||||||
- name: Generate key
|
|
||||||
run: php artisan key:generate
|
|
||||||
|
|
||||||
- name: Directory Permissions
|
|
||||||
run: chmod -R 777 storage bootstrap/cache
|
|
||||||
|
|
||||||
- name: Run Migrations
|
|
||||||
env:
|
|
||||||
DB_CONNECTION: mysql
|
|
||||||
DB_DATABASE: test
|
|
||||||
DB_PORT: 33306
|
|
||||||
DB_USER: root
|
|
||||||
run: php artisan migrate
|
|
||||||
|
|
||||||
- name: Execute Unit & Feature Tests
|
|
||||||
env:
|
|
||||||
DB_CONNECTION: mysql
|
|
||||||
DB_DATABASE: test
|
|
||||||
DB_PORT: 33306
|
|
||||||
DB_USER: root
|
|
||||||
run: |
|
|
||||||
composer require pestphp/pest --dev --with-all-dependencies
|
|
||||||
./vendor/bin/pest
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,3 +17,4 @@ yarn-error.log
|
|||||||
/.fleet
|
/.fleet
|
||||||
/.idea
|
/.idea
|
||||||
/.vscode
|
/.vscode
|
||||||
|
.DS_Store
|
||||||
|
373
LICENSE
Normal file
373
LICENSE
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
@ -2,20 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\License;
|
use App\Jobs\ImportHubData;
|
||||||
use App\Models\Mod;
|
|
||||||
use App\Models\ModVersion;
|
|
||||||
use App\Models\SptVersion;
|
|
||||||
use App\Models\User;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Benchmark;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use League\HTMLToMarkdown\HtmlConverter;
|
|
||||||
use Stevebauman\Purify\Facades\Purify;
|
|
||||||
|
|
||||||
class ImportHub extends Command
|
class ImportHub extends Command
|
||||||
{
|
{
|
||||||
@ -23,464 +11,11 @@ class ImportHub extends Command
|
|||||||
|
|
||||||
protected $description = 'Connects to the Hub database and imports the data into the Laravel database.';
|
protected $description = 'Connects to the Hub database and imports the data into the Laravel database.';
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
// This may take a minute or two...
|
// Add the ImportHubData job to the queue.
|
||||||
set_time_limit(0);
|
ImportHubData::dispatch()->onQueue('long');
|
||||||
|
|
||||||
$this->newLine();
|
$this->info('The import job has been added to the queue.');
|
||||||
|
|
||||||
$totalTime = Benchmark::value(function () {
|
|
||||||
$loadDataTime = Benchmark::value(function () {
|
|
||||||
$this->loadData();
|
|
||||||
});
|
|
||||||
$this->info('Execution time: '.round($loadDataTime[1], 2).'ms');
|
|
||||||
$this->newLine();
|
|
||||||
|
|
||||||
$importUsersTime = Benchmark::value(function () {
|
|
||||||
$this->importUsers();
|
|
||||||
});
|
|
||||||
$this->info('Execution time: '.round($importUsersTime[1], 2).'ms');
|
|
||||||
$this->newLine();
|
|
||||||
|
|
||||||
$importLicensesTime = Benchmark::value(function () {
|
|
||||||
$this->importLicenses();
|
|
||||||
});
|
|
||||||
$this->info('Execution time: '.round($importLicensesTime[1], 2).'ms');
|
|
||||||
$this->newLine();
|
|
||||||
|
|
||||||
$importSptVersionsTime = Benchmark::value(function () {
|
|
||||||
$this->importSptVersions();
|
|
||||||
});
|
|
||||||
$this->info('Execution time: '.round($importSptVersionsTime[1], 2).'ms');
|
|
||||||
$this->newLine();
|
|
||||||
|
|
||||||
$importModsTime = Benchmark::value(function () {
|
|
||||||
$this->importMods();
|
|
||||||
});
|
|
||||||
$this->info('Execution time: '.round($importModsTime[1], 2).'ms');
|
|
||||||
$this->newLine();
|
|
||||||
|
|
||||||
$importModVersionsTime = Benchmark::value(function () {
|
|
||||||
$this->importModVersions();
|
|
||||||
});
|
|
||||||
$this->info('Execution time: '.round($importModVersionsTime[1], 2).'ms');
|
|
||||||
$this->newLine();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disconnect from the Hub database, clearing temporary tables.
|
|
||||||
DB::connection('mysql_hub')->disconnect();
|
|
||||||
|
|
||||||
$this->newLine();
|
|
||||||
$this->info('Data imported successfully');
|
|
||||||
$this->info('Total execution time: '.round($totalTime[1], 2).'ms');
|
|
||||||
|
|
||||||
$this->newLine();
|
|
||||||
$this->info('Refreshing Meilisearch indexes...');
|
|
||||||
$this->call('scout:delete-all-indexes');
|
|
||||||
$this->call('scout:sync-index-settings');
|
|
||||||
$this->call('scout:import', ['model' => '\App\Models\Mod']);
|
|
||||||
|
|
||||||
$this->newLine();
|
|
||||||
$this->info('Done');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function loadData(): void
|
|
||||||
{
|
|
||||||
// We're just going to dump a few things in memory to escape the N+1 problem.
|
|
||||||
$this->output->write('Loading data into memory... ');
|
|
||||||
$this->bringFileOptionsLocal();
|
|
||||||
$this->bringFileContentLocal();
|
|
||||||
$this->bringFileVersionLabelsLocal();
|
|
||||||
$this->bringFileVersionContentLocal();
|
|
||||||
$this->info('Done.');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function importUsers(): void
|
|
||||||
{
|
|
||||||
$totalInserted = 0;
|
|
||||||
|
|
||||||
foreach (DB::connection('mysql_hub')->table('wcf1_user')->orderBy('userID')->cursor() as $wolt) {
|
|
||||||
$registrationDate = Carbon::parse($wolt->registrationDate, 'UTC');
|
|
||||||
if ($registrationDate->isFuture()) {
|
|
||||||
$registrationDate = now('UTC');
|
|
||||||
}
|
|
||||||
$registrationDate->setTimezone('UTC');
|
|
||||||
|
|
||||||
$insertData = [
|
|
||||||
'hub_id' => $wolt->userID,
|
|
||||||
'name' => $wolt->username,
|
|
||||||
'email' => mb_convert_case($wolt->email, MB_CASE_LOWER, 'UTF-8'),
|
|
||||||
'password' => $this->cleanPasswordHash($wolt->password),
|
|
||||||
'created_at' => $registrationDate,
|
|
||||||
'updated_at' => now('UTC')->toDateTimeString(),
|
|
||||||
];
|
|
||||||
|
|
||||||
User::upsert($insertData, ['hub_id'], ['name', 'email', 'password', 'created_at', 'updated_at']);
|
|
||||||
$totalInserted++;
|
|
||||||
|
|
||||||
// Log every 2500 users processed
|
|
||||||
if ($totalInserted % 2500 == 0) {
|
|
||||||
$this->line('Processed 2500 users. Total processed so far: '.$totalInserted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info('Total users processed: '.$totalInserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function cleanPasswordHash(string $password): string
|
|
||||||
{
|
|
||||||
// The hub passwords are hashed sometimes with a prefix of the hash type. We only want the hash.
|
|
||||||
// If it's not Bcrypt, they'll have to reset their password. Tough luck.
|
|
||||||
return str_replace(['Bcrypt:', 'cryptMD5:', 'cryptMD5::'], '', $password);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function importLicenses(): void
|
|
||||||
{
|
|
||||||
$totalInserted = 0;
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
|
||||||
->table('filebase1_license')
|
|
||||||
->chunkById(100, function (Collection $licenses) use (&$totalInserted) {
|
|
||||||
$insertData = [];
|
|
||||||
foreach ($licenses as $license) {
|
|
||||||
$insertData[] = [
|
|
||||||
'hub_id' => $license->licenseID,
|
|
||||||
'name' => $license->licenseName,
|
|
||||||
'link' => $license->licenseURL,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($insertData)) {
|
|
||||||
DB::table('licenses')->upsert($insertData, ['hub_id'], ['name', 'link']);
|
|
||||||
$totalInserted += count($insertData);
|
|
||||||
$this->line('Processed '.count($insertData).' licenses. Total processed so far: '.$totalInserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
unset($insertData);
|
|
||||||
unset($licenses);
|
|
||||||
}, 'licenseID');
|
|
||||||
|
|
||||||
$this->info('Total licenses processed: '.$totalInserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function importSptVersions(): void
|
|
||||||
{
|
|
||||||
$totalInserted = 0;
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
|
||||||
->table('wcf1_label')
|
|
||||||
->where('groupID', 1)
|
|
||||||
->chunkById(100, function (Collection $versions) use (&$totalInserted) {
|
|
||||||
$insertData = [];
|
|
||||||
foreach ($versions as $version) {
|
|
||||||
$insertData[] = [
|
|
||||||
'hub_id' => $version->labelID,
|
|
||||||
'version' => $version->label,
|
|
||||||
'color_class' => $this->translateColour($version->cssClassName),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($insertData)) {
|
|
||||||
DB::table('spt_versions')->upsert($insertData, ['hub_id'], ['version', 'color_class']);
|
|
||||||
$totalInserted += count($insertData);
|
|
||||||
$this->line('Processed '.count($insertData).' SPT Versions. Total processed so far: '.$totalInserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
unset($insertData);
|
|
||||||
unset($versions);
|
|
||||||
}, 'labelID');
|
|
||||||
|
|
||||||
$this->info('Total licenses processed: '.$totalInserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function translateColour(string $colour = ''): string
|
|
||||||
{
|
|
||||||
return match ($colour) {
|
|
||||||
'green' => 'green',
|
|
||||||
'slightly-outdated' => 'lime',
|
|
||||||
'yellow' => 'yellow',
|
|
||||||
'red' => 'red',
|
|
||||||
default => 'gray',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function importMods(): void
|
|
||||||
{
|
|
||||||
$command = $this;
|
|
||||||
$totalInserted = 0;
|
|
||||||
|
|
||||||
$curl = curl_init();
|
|
||||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
|
||||||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
|
||||||
->table('filebase1_file')
|
|
||||||
->chunkById(100, function (Collection $mods) use (&$command, &$curl, &$totalInserted) {
|
|
||||||
|
|
||||||
foreach ($mods as $mod) {
|
|
||||||
|
|
||||||
$modContent = DB::table('temp_file_content')
|
|
||||||
->where('fileID', $mod->fileID)
|
|
||||||
->orderBy('fileID', 'desc')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$optionSourceCode = DB::table('temp_file_option_values')
|
|
||||||
->select('optionValue as source_code_link')
|
|
||||||
->where('fileID', $mod->fileID)
|
|
||||||
->whereIn('optionID', [5, 1])
|
|
||||||
->whereNot('optionValue', '')
|
|
||||||
->orderByDesc('optionID')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$optionContainsAi = DB::table('temp_file_option_values')
|
|
||||||
->select('optionValue as contains_ai')
|
|
||||||
->where('fileID', $mod->fileID)
|
|
||||||
->where('optionID', 7)
|
|
||||||
->whereNot('optionValue', '')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$optionContainsAds = DB::table('temp_file_option_values')
|
|
||||||
->select('optionValue as contains_ads')
|
|
||||||
->where('fileID', $mod->fileID)
|
|
||||||
->where('optionID', 3)
|
|
||||||
->whereNot('optionValue', '')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$versionLabel = DB::table('temp_file_version_labels')
|
|
||||||
->select('labelID')
|
|
||||||
->where('objectID', $mod->fileID)
|
|
||||||
->orderBy('labelID', 'desc')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (empty($versionLabel)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$insertData[] = [
|
|
||||||
'hub_id' => (int) $mod->fileID,
|
|
||||||
'user_id' => User::whereHubId($mod->userID)->value('id'),
|
|
||||||
'name' => $modContent?->subject ?? '',
|
|
||||||
'slug' => Str::slug($modContent?->subject) ?? '',
|
|
||||||
'teaser' => Str::limit($modContent?->teaser) ?? '',
|
|
||||||
'description' => $this->convertModDescription($modContent?->message ?? ''),
|
|
||||||
'thumbnail' => $this->fetchModThumbnail($command, $curl, $mod->fileID, $mod->iconHash, $mod->iconExtension),
|
|
||||||
'license_id' => License::whereHubId($mod->licenseID)->value('id'),
|
|
||||||
'source_code_link' => $optionSourceCode?->source_code_link ?? '',
|
|
||||||
'featured' => (bool) $mod->isFeatured,
|
|
||||||
'contains_ai_content' => (bool) $optionContainsAi?->contains_ai ?? false,
|
|
||||||
'contains_ads' => (bool) $optionContainsAds?->contains_ads ?? false,
|
|
||||||
'disabled' => (bool) $mod->isDisabled,
|
|
||||||
'created_at' => Carbon::parse($mod->time, 'UTC'),
|
|
||||||
'updated_at' => Carbon::parse($mod->lastChangeTime, 'UTC'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($insertData)) {
|
|
||||||
Mod::upsert($insertData, ['hub_id'], ['user_id', 'name', 'slug', 'teaser', 'description', 'thumbnail', 'license_id', 'source_code_link', 'featured', 'contains_ai_content', 'disabled', 'created_at', 'updated_at']);
|
|
||||||
$totalInserted += count($insertData);
|
|
||||||
$command->line('Processed '.count($insertData).' mods. Total processed so far: '.$totalInserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
unset($insertData);
|
|
||||||
unset($mods);
|
|
||||||
}, 'fileID');
|
|
||||||
|
|
||||||
curl_close($curl);
|
|
||||||
|
|
||||||
$this->info('Total mods processed: '.$totalInserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function bringFileOptionsLocal(): void
|
|
||||||
{
|
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_option_values (
|
|
||||||
fileID INT,
|
|
||||||
optionID INT,
|
|
||||||
optionValue VARCHAR(255)
|
|
||||||
)');
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
|
||||||
->table('filebase1_file_option_value')
|
|
||||||
->orderBy('fileID')
|
|
||||||
->chunk(200, function ($options) {
|
|
||||||
foreach ($options as $option) {
|
|
||||||
DB::table('temp_file_option_values')->insert([
|
|
||||||
'fileID' => $option->fileID,
|
|
||||||
'optionID' => $option->optionID,
|
|
||||||
'optionValue' => $option->optionValue,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function bringFileContentLocal(): void
|
|
||||||
{
|
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_content (
|
|
||||||
fileID INT,
|
|
||||||
subject VARCHAR(255),
|
|
||||||
teaser VARCHAR(255),
|
|
||||||
message LONGTEXT
|
|
||||||
)');
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
|
||||||
->table('filebase1_file_content')
|
|
||||||
->orderBy('fileID')
|
|
||||||
->chunk(200, function ($contents) {
|
|
||||||
foreach ($contents as $content) {
|
|
||||||
DB::table('temp_file_content')->insert([
|
|
||||||
'fileID' => $content->fileID,
|
|
||||||
'subject' => $content->subject,
|
|
||||||
'teaser' => $content->teaser,
|
|
||||||
'message' => $content->message,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function fetchModThumbnail($command, $curl, string $fileID, string $thumbnailHash, string $thumbnailExtension): string
|
|
||||||
{
|
|
||||||
if (empty($fileID) || empty($thumbnailHash) || empty($thumbnailExtension)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only the first two characters of the icon hash.
|
|
||||||
$hashShort = substr($thumbnailHash, 0, 2);
|
|
||||||
|
|
||||||
$hubUrl = "https://hub.sp-tarkov.com/files/images/file/$hashShort/$fileID.$thumbnailExtension";
|
|
||||||
$relativePath = "mods/$thumbnailHash.$thumbnailExtension";
|
|
||||||
|
|
||||||
// Check to make sure the image doesn't already exist.
|
|
||||||
if (Storage::exists($relativePath)) {
|
|
||||||
return $relativePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
$command->output->write("Downloading mod thumbnail: $hubUrl... ");
|
|
||||||
curl_setopt($curl, CURLOPT_URL, $hubUrl);
|
|
||||||
$image = curl_exec($curl);
|
|
||||||
if ($image === false) {
|
|
||||||
$command->error('Error: '.curl_error($curl));
|
|
||||||
} else {
|
|
||||||
Storage::put($relativePath, $image);
|
|
||||||
$command->info('Done.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $relativePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function importModVersions(): void
|
|
||||||
{
|
|
||||||
$command = $this;
|
|
||||||
$totalInserted = 0;
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
|
||||||
->table('filebase1_file_version')
|
|
||||||
->chunkById(500, function (Collection $versions) use (&$command, &$totalInserted) {
|
|
||||||
|
|
||||||
foreach ($versions as $version) {
|
|
||||||
|
|
||||||
$versionContent = DB::table('temp_file_version_content')
|
|
||||||
->select('description')
|
|
||||||
->where('versionID', $version->versionID)
|
|
||||||
->orderBy('versionID', 'desc')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$optionVirusTotal = DB::table('temp_file_option_values')
|
|
||||||
->select('optionValue as virus_total_link')
|
|
||||||
->where('fileID', $version->fileID)
|
|
||||||
->whereIn('optionID', [6, 2])
|
|
||||||
->whereNot('optionValue', '')
|
|
||||||
->orderByDesc('optionID')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$versionLabel = DB::table('temp_file_version_labels')
|
|
||||||
->select('labelID')
|
|
||||||
->where('objectID', $version->fileID)
|
|
||||||
->orderBy('labelID', 'desc')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$modId = Mod::whereHubId($version->fileID)->value('id');
|
|
||||||
|
|
||||||
if (empty($versionLabel) || empty($modId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$insertData[] = [
|
|
||||||
'hub_id' => $version->versionID,
|
|
||||||
'mod_id' => $modId,
|
|
||||||
'version' => $version->versionNumber,
|
|
||||||
'description' => $this->convertModDescription($versionContent->description ?? ''),
|
|
||||||
'link' => $version->downloadURL,
|
|
||||||
'spt_version_id' => SptVersion::whereHubId($versionLabel->labelID)->value('id'),
|
|
||||||
'virus_total_link' => $optionVirusTotal?->virus_total_link ?? '',
|
|
||||||
'downloads' => max((int) $version->downloads, 0), // Ensure the value is at least 0
|
|
||||||
'disabled' => (bool) $version->isDisabled,
|
|
||||||
'created_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
|
||||||
'updated_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($insertData)) {
|
|
||||||
ModVersion::upsert($insertData, ['hub_id'], ['mod_id', 'version', 'description', 'link', 'spt_version_id', 'virus_total_link', 'downloads', 'created_at', 'updated_at']);
|
|
||||||
$totalInserted += count($insertData);
|
|
||||||
$command->line('Processed '.count($insertData).' mod versions. Total processed so far: '.$totalInserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
unset($insertData);
|
|
||||||
unset($version);
|
|
||||||
}, 'versionID');
|
|
||||||
|
|
||||||
$this->info('Total mod versions processed: '.$totalInserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function bringFileVersionLabelsLocal(): void
|
|
||||||
{
|
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_version_labels (
|
|
||||||
labelID INT,
|
|
||||||
objectID INT
|
|
||||||
)');
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
|
||||||
->table('wcf1_label_object')
|
|
||||||
->where('objectTypeID', 387)
|
|
||||||
->orderBy('labelID')
|
|
||||||
->chunk(200, function ($options) {
|
|
||||||
foreach ($options as $option) {
|
|
||||||
DB::table('temp_file_version_labels')->insert([
|
|
||||||
'labelID' => $option->labelID,
|
|
||||||
'objectID' => $option->objectID,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function bringFileVersionContentLocal(): void
|
|
||||||
{
|
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_version_content (
|
|
||||||
versionID INT,
|
|
||||||
description TEXT
|
|
||||||
)');
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
|
||||||
->table('filebase1_file_version_content')
|
|
||||||
->orderBy('versionID')
|
|
||||||
->chunk(200, function ($options) {
|
|
||||||
foreach ($options as $option) {
|
|
||||||
DB::table('temp_file_version_content')->insert([
|
|
||||||
'versionID' => $option->versionID,
|
|
||||||
'description' => $option->description,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function convertModDescription(string $description): string
|
|
||||||
{
|
|
||||||
// Alright, hear me out... Shut up.
|
|
||||||
$converter = new HtmlConverter();
|
|
||||||
|
|
||||||
return $converter->convert(Purify::clean($description));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,24 +8,15 @@ use Illuminate\Support\Facades\Storage;
|
|||||||
|
|
||||||
class UploadAssets extends Command
|
class UploadAssets extends Command
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'app:upload-assets';
|
protected $signature = 'app:upload-assets';
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Uploads the Vite build assets to Cloudflare R2';
|
protected $description = 'Uploads the Vite build assets to Cloudflare R2';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the console command.
|
* This command uploads the Vite build assets to Cloudflare R2. Typically, this will be run after the assets have
|
||||||
|
* been built and the application is ready to deploy from within the production environment build process.
|
||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle(): void
|
||||||
{
|
{
|
||||||
$this->info('Publishing assets...');
|
$this->info('Publishing assets...');
|
||||||
|
|
||||||
|
678
app/Jobs/ImportHubData.php
Normal file
678
app/Jobs/ImportHubData.php
Normal file
@ -0,0 +1,678 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\License;
|
||||||
|
use App\Models\Mod;
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use App\Models\SptVersion;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\UserRole;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use CurlHandle;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use League\HTMLToMarkdown\HtmlConverter;
|
||||||
|
use Stevebauman\Purify\Facades\Purify;
|
||||||
|
|
||||||
|
class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
// Stream some data locally so that we don't have to keep accessing the Hub's database. Use MySQL temporary
|
||||||
|
// tables to store the data to save on memory; we don't want this to be a hog.
|
||||||
|
$this->bringFileOptionsLocal();
|
||||||
|
$this->bringFileContentLocal();
|
||||||
|
$this->bringFileVersionLabelsLocal();
|
||||||
|
$this->bringFileVersionContentLocal();
|
||||||
|
|
||||||
|
// Begin to import the data into the permanent local database tables.
|
||||||
|
$this->importUsers();
|
||||||
|
$this->importLicenses();
|
||||||
|
$this->importSptVersions();
|
||||||
|
$this->importMods();
|
||||||
|
$this->importModVersions();
|
||||||
|
|
||||||
|
// Ensure that we've disconnected from the Hub database, clearing temporary tables.
|
||||||
|
DB::connection('mysql_hub')->disconnect();
|
||||||
|
|
||||||
|
// Reindex the Meilisearch index.
|
||||||
|
Artisan::call('scout:delete-all-indexes');
|
||||||
|
Artisan::call('scout:sync-index-settings');
|
||||||
|
Artisan::call('scout:import', ['model' => '\App\Models\Mod']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bring the file options from the Hub database to the local database temporary table.
|
||||||
|
*/
|
||||||
|
protected function bringFileOptionsLocal(): void
|
||||||
|
{
|
||||||
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
|
||||||
|
DB::statement('CREATE TEMPORARY TABLE temp_file_option_values (
|
||||||
|
fileID INT,
|
||||||
|
optionID INT,
|
||||||
|
optionValue VARCHAR(255)
|
||||||
|
)');
|
||||||
|
|
||||||
|
DB::connection('mysql_hub')
|
||||||
|
->table('filebase1_file_option_value')
|
||||||
|
->orderBy('fileID')
|
||||||
|
->chunk(200, function ($options) {
|
||||||
|
foreach ($options as $option) {
|
||||||
|
DB::table('temp_file_option_values')->insert([
|
||||||
|
'fileID' => (int) $option->fileID,
|
||||||
|
'optionID' => (int) $option->optionID,
|
||||||
|
'optionValue' => $option->optionValue,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bring the file content from the Hub database to the local database temporary table.
|
||||||
|
*/
|
||||||
|
protected function bringFileContentLocal(): void
|
||||||
|
{
|
||||||
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
||||||
|
DB::statement('CREATE TEMPORARY TABLE temp_file_content (
|
||||||
|
fileID INT,
|
||||||
|
subject VARCHAR(255),
|
||||||
|
teaser VARCHAR(255),
|
||||||
|
message LONGTEXT
|
||||||
|
)');
|
||||||
|
|
||||||
|
DB::connection('mysql_hub')
|
||||||
|
->table('filebase1_file_content')
|
||||||
|
->orderBy('fileID')
|
||||||
|
->chunk(200, function ($contents) {
|
||||||
|
foreach ($contents as $content) {
|
||||||
|
DB::table('temp_file_content')->insert([
|
||||||
|
'fileID' => (int) $content->fileID,
|
||||||
|
'subject' => $content->subject,
|
||||||
|
'teaser' => $content->teaser,
|
||||||
|
'message' => $content->message,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bring the file version labels from the Hub database to the local database temporary table.
|
||||||
|
*/
|
||||||
|
protected function bringFileVersionLabelsLocal(): void
|
||||||
|
{
|
||||||
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_labels');
|
||||||
|
DB::statement('CREATE TEMPORARY TABLE temp_file_version_labels (
|
||||||
|
labelID INT,
|
||||||
|
objectID INT
|
||||||
|
)');
|
||||||
|
|
||||||
|
DB::connection('mysql_hub')
|
||||||
|
->table('wcf1_label_object')
|
||||||
|
->where('objectTypeID', 387)
|
||||||
|
->orderBy('labelID')
|
||||||
|
->chunk(200, function ($options) {
|
||||||
|
foreach ($options as $option) {
|
||||||
|
DB::table('temp_file_version_labels')->insert([
|
||||||
|
'labelID' => (int) $option->labelID,
|
||||||
|
'objectID' => (int) $option->objectID,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bring the file version content from the Hub database to the local database temporary table.
|
||||||
|
*/
|
||||||
|
protected function bringFileVersionContentLocal(): void
|
||||||
|
{
|
||||||
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_content');
|
||||||
|
DB::statement('CREATE TEMPORARY TABLE temp_file_version_content (
|
||||||
|
versionID INT,
|
||||||
|
description TEXT
|
||||||
|
)');
|
||||||
|
|
||||||
|
DB::connection('mysql_hub')
|
||||||
|
->table('filebase1_file_version_content')
|
||||||
|
->orderBy('versionID')
|
||||||
|
->chunk(200, function ($options) {
|
||||||
|
foreach ($options as $option) {
|
||||||
|
DB::table('temp_file_version_content')->insert([
|
||||||
|
'versionID' => (int) $option->versionID,
|
||||||
|
'description' => $option->description,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import the users from the Hub database to the local database.
|
||||||
|
*/
|
||||||
|
protected function importUsers(): void
|
||||||
|
{
|
||||||
|
DB::connection('mysql_hub')
|
||||||
|
->table('wcf1_user as u')
|
||||||
|
->select('u.userID', 'u.username', 'u.email', 'u.password', 'u.registrationDate', 'u.banned', 'u.banReason', 'u.banExpires', 'u.rankID', 'r.rankTitle')
|
||||||
|
->leftJoin('wcf1_user_rank as r', 'u.rankID', '=', 'r.rankID')
|
||||||
|
->chunkById(250, function (Collection $users) {
|
||||||
|
$userData = $bannedUsers = $userRanks = [];
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$userData[] = $this->collectUserData($user);
|
||||||
|
|
||||||
|
$bannedUserData = $this->collectBannedUserData($user);
|
||||||
|
if ($bannedUserData) {
|
||||||
|
$bannedUsers[] = $bannedUserData;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userRankData = $this->collectUserRankData($user);
|
||||||
|
if ($userRankData) {
|
||||||
|
$userRanks[] = $userRankData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->upsertUsers($userData);
|
||||||
|
$this->handleBannedUsers($bannedUsers);
|
||||||
|
$this->handleUserRoles($userRanks);
|
||||||
|
}, 'userID');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function collectUserData($user): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'hub_id' => (int) $user->userID,
|
||||||
|
'name' => $user->username,
|
||||||
|
'email' => mb_convert_case($user->email, MB_CASE_LOWER, 'UTF-8'),
|
||||||
|
'password' => $this->cleanPasswordHash($user->password),
|
||||||
|
'created_at' => $this->cleanRegistrationDate($user->registrationDate),
|
||||||
|
'updated_at' => now('UTC')->toDateTimeString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean the password hash from the Hub database.
|
||||||
|
*/
|
||||||
|
protected function cleanPasswordHash(string $password): string
|
||||||
|
{
|
||||||
|
// The hub passwords sometimes hashed with a prefix of the hash type. We only want the hash.
|
||||||
|
// If it's not Bcrypt, they'll have to reset their password. Tough luck.
|
||||||
|
$clean = str_ireplace(['invalid:', 'bcrypt:', 'bcrypt::', 'cryptmd5:', 'cryptmd5::'], '', $password);
|
||||||
|
|
||||||
|
// At this point, if the password hash starts with $2, it's a valid Bcrypt hash. Otherwise, it's invalid.
|
||||||
|
return str_starts_with($clean, '$2') ? $clean : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean the registration date from the Hub database.
|
||||||
|
*/
|
||||||
|
protected function cleanRegistrationDate(string $registrationDate): string
|
||||||
|
{
|
||||||
|
$date = Carbon::createFromTimestamp($registrationDate);
|
||||||
|
|
||||||
|
// If the registration date is in the future, set it to now.
|
||||||
|
if ($date->isFuture()) {
|
||||||
|
$date = Carbon::now('UTC');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $date->toDateTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an array of banned user data ready to be inserted into the local database.
|
||||||
|
*/
|
||||||
|
protected function collectBannedUserData($user): ?array
|
||||||
|
{
|
||||||
|
if ($user->banned) {
|
||||||
|
return [
|
||||||
|
'hub_id' => (int) $user->userID,
|
||||||
|
'comment' => $user->banReason ?? '',
|
||||||
|
'expired_at' => $this->cleanUnbannedAtDate($user->banExpires),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean the banned_at date from the Hub database.
|
||||||
|
*/
|
||||||
|
protected function cleanUnbannedAtDate(?string $unbannedAt): ?string
|
||||||
|
{
|
||||||
|
// If the input is null, return null
|
||||||
|
if ($unbannedAt === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit check for the Unix epoch start date
|
||||||
|
if (Str::contains($unbannedAt, '1970-01-01')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use validator to check for a valid date format
|
||||||
|
$validator = Validator::make(['date' => $unbannedAt], [
|
||||||
|
'date' => 'date_format:Y-m-d H:i:s',
|
||||||
|
]);
|
||||||
|
if ($validator->fails()) {
|
||||||
|
// If the date format is invalid, return null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the date using Carbon
|
||||||
|
try {
|
||||||
|
$date = Carbon::parse($unbannedAt);
|
||||||
|
|
||||||
|
// Additional check to ensure the date is not a default or zero date
|
||||||
|
if ($date->year == 0 || $date->month == 0 || $date->day == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $date->toDateTimeString();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// If the date is not valid, return null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Build an array of user rank data ready to be inserted into the local database.
|
||||||
|
*/
|
||||||
|
protected function collectUserRankData($user): ?array
|
||||||
|
{
|
||||||
|
if ($user->rankID && $user->rankTitle) {
|
||||||
|
return [
|
||||||
|
'hub_id' => (int) $user->userID,
|
||||||
|
'title' => $user->rankTitle,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert or update the users in the local database.
|
||||||
|
*/
|
||||||
|
protected function upsertUsers($usersData): void
|
||||||
|
{
|
||||||
|
if (! empty($usersData)) {
|
||||||
|
DB::table('users')->upsert($usersData, ['hub_id'], [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the hub-banned users from the local database and ban them locally.
|
||||||
|
*/
|
||||||
|
protected function handleBannedUsers($bannedUsers): void
|
||||||
|
{
|
||||||
|
foreach ($bannedUsers as $bannedUser) {
|
||||||
|
$user = User::whereHubId($bannedUser['hub_id'])->first();
|
||||||
|
$user->ban([
|
||||||
|
'comment' => $bannedUser['comment'],
|
||||||
|
'expired_at' => $bannedUser['expired_at'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch or create the user ranks in the local database and assign them to the users.
|
||||||
|
*/
|
||||||
|
protected function handleUserRoles($userRanks): void
|
||||||
|
{
|
||||||
|
foreach ($userRanks as $userRank) {
|
||||||
|
$roleName = Str::ucfirst(Str::afterLast($userRank['title'], '.'));
|
||||||
|
$roleData = $this->buildUserRoleData($roleName);
|
||||||
|
UserRole::upsert($roleData, ['name'], ['name', 'short_name', 'description', 'color_class']);
|
||||||
|
|
||||||
|
$userRole = UserRole::whereName($roleData['name'])->first();
|
||||||
|
$user = User::whereHubId($userRank['hub_id'])->first();
|
||||||
|
$user->assignRole($userRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the user role data based on the role name.
|
||||||
|
*/
|
||||||
|
protected function buildUserRoleData(string $name): array
|
||||||
|
{
|
||||||
|
if ($name === 'Administrator') {
|
||||||
|
return [
|
||||||
|
'name' => 'Administrator',
|
||||||
|
'short_name' => 'Admin',
|
||||||
|
'description' => 'An administrator has full access to the site.',
|
||||||
|
'color_class' => 'sky',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($name === 'Moderator') {
|
||||||
|
return [
|
||||||
|
'name' => 'Moderator',
|
||||||
|
'short_name' => 'Mod',
|
||||||
|
'description' => 'A moderator has the ability to moderate user content.',
|
||||||
|
'color_class' => 'emerald',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $name,
|
||||||
|
'short_name' => '',
|
||||||
|
'description' => '',
|
||||||
|
'color_class' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import the licenses from the Hub database to the local database.
|
||||||
|
*/
|
||||||
|
protected function importLicenses(): void
|
||||||
|
{
|
||||||
|
DB::connection('mysql_hub')
|
||||||
|
->table('filebase1_license')
|
||||||
|
->chunkById(100, function (Collection $licenses) {
|
||||||
|
|
||||||
|
$insertData = [];
|
||||||
|
foreach ($licenses as $license) {
|
||||||
|
$insertData[] = [
|
||||||
|
'hub_id' => (int) $license->licenseID,
|
||||||
|
'name' => $license->licenseName,
|
||||||
|
'link' => $license->licenseURL,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($insertData)) {
|
||||||
|
DB::table('licenses')->upsert($insertData, ['hub_id'], ['name', 'link']);
|
||||||
|
}
|
||||||
|
}, 'licenseID');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import the SPT versions from the Hub database to the local database.
|
||||||
|
*/
|
||||||
|
protected function importSptVersions(): void
|
||||||
|
{
|
||||||
|
DB::connection('mysql_hub')
|
||||||
|
->table('wcf1_label')
|
||||||
|
->where('groupID', 1)
|
||||||
|
->chunkById(100, function (Collection $versions) {
|
||||||
|
$insertData = [];
|
||||||
|
foreach ($versions as $version) {
|
||||||
|
$insertData[] = [
|
||||||
|
'hub_id' => (int) $version->labelID,
|
||||||
|
'version' => $version->label,
|
||||||
|
'color_class' => $this->translateColour($version->cssClassName),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($insertData)) {
|
||||||
|
DB::table('spt_versions')->upsert($insertData, ['hub_id'], ['version', 'color_class']);
|
||||||
|
}
|
||||||
|
}, 'labelID');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate the colour class from the Hub database to the local database.
|
||||||
|
*/
|
||||||
|
protected function translateColour(string $colour = ''): string
|
||||||
|
{
|
||||||
|
return match ($colour) {
|
||||||
|
'green' => 'green',
|
||||||
|
'slightly-outdated' => 'lime',
|
||||||
|
'yellow' => 'yellow',
|
||||||
|
'red' => 'red',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import the mods from the Hub database to the local database.
|
||||||
|
*/
|
||||||
|
protected function importMods(): void
|
||||||
|
{
|
||||||
|
// Initialize a cURL handler for downloading mod thumbnails.
|
||||||
|
$curl = curl_init();
|
||||||
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
||||||
|
|
||||||
|
DB::connection('mysql_hub')
|
||||||
|
->table('filebase1_file')
|
||||||
|
->chunkById(100, function (Collection $mods) use ($curl) {
|
||||||
|
|
||||||
|
foreach ($mods as $mod) {
|
||||||
|
$modContent = DB::table('temp_file_content')
|
||||||
|
->where('fileID', $mod->fileID)
|
||||||
|
->orderBy('fileID', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$optionSourceCode = DB::table('temp_file_option_values')
|
||||||
|
->select('optionValue as source_code_link')
|
||||||
|
->where('fileID', $mod->fileID)
|
||||||
|
->whereIn('optionID', [5, 1])
|
||||||
|
->whereNot('optionValue', '')
|
||||||
|
->orderByDesc('optionID')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$optionContainsAi = DB::table('temp_file_option_values')
|
||||||
|
->select('optionValue as contains_ai')
|
||||||
|
->where('fileID', $mod->fileID)
|
||||||
|
->where('optionID', 7)
|
||||||
|
->whereNot('optionValue', '')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$optionContainsAds = DB::table('temp_file_option_values')
|
||||||
|
->select('optionValue as contains_ads')
|
||||||
|
->where('fileID', $mod->fileID)
|
||||||
|
->where('optionID', 3)
|
||||||
|
->whereNot('optionValue', '')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$versionLabel = DB::table('temp_file_version_labels')
|
||||||
|
->select('labelID')
|
||||||
|
->where('objectID', $mod->fileID)
|
||||||
|
->orderBy('labelID', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// Skip the mod if it doesn't have a version label attached to it.
|
||||||
|
if (empty($versionLabel)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$insertData[] = [
|
||||||
|
'hub_id' => (int) $mod->fileID,
|
||||||
|
'user_id' => User::whereHubId($mod->userID)->value('id'),
|
||||||
|
'name' => $modContent?->subject ?? '',
|
||||||
|
'slug' => Str::slug($modContent?->subject ?? ''),
|
||||||
|
'teaser' => Str::limit($modContent?->teaser ?? ''),
|
||||||
|
'description' => $this->cleanHubContent($modContent?->message ?? ''),
|
||||||
|
'thumbnail' => $this->fetchModThumbnail($curl, $mod->fileID, $mod->iconHash, $mod->iconExtension),
|
||||||
|
'license_id' => License::whereHubId($mod->licenseID)->value('id'),
|
||||||
|
'source_code_link' => $optionSourceCode?->source_code_link ?? '',
|
||||||
|
'featured' => (bool) $mod?->isFeatured,
|
||||||
|
'contains_ai_content' => (bool) $optionContainsAi?->contains_ai,
|
||||||
|
'contains_ads' => (bool) $optionContainsAds?->contains_ads,
|
||||||
|
'disabled' => (bool) $mod?->isDisabled,
|
||||||
|
'created_at' => Carbon::parse($mod->time, 'UTC'),
|
||||||
|
'updated_at' => Carbon::parse($mod->lastChangeTime, 'UTC'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($insertData)) {
|
||||||
|
Mod::upsert($insertData, ['hub_id'], [
|
||||||
|
'user_id',
|
||||||
|
'name',
|
||||||
|
'slug',
|
||||||
|
'teaser',
|
||||||
|
'description',
|
||||||
|
'thumbnail',
|
||||||
|
'license_id',
|
||||||
|
'source_code_link',
|
||||||
|
'featured',
|
||||||
|
'contains_ai_content',
|
||||||
|
'disabled',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}, 'fileID');
|
||||||
|
|
||||||
|
// Close the cURL handler.
|
||||||
|
curl_close($curl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the mod description from WoltHub flavoured HTML to Markdown.
|
||||||
|
*/
|
||||||
|
protected function cleanHubContent(string $dirty): string
|
||||||
|
{
|
||||||
|
// Alright, hear me out... Shut up.
|
||||||
|
|
||||||
|
$converter = new HtmlConverter();
|
||||||
|
$clean = Purify::clean($dirty);
|
||||||
|
|
||||||
|
return $converter->convert($clean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the mod thumbnail from the Hub and store it anew.
|
||||||
|
*/
|
||||||
|
protected function fetchModThumbnail(CurlHandle $curl, string $fileID, string $thumbnailHash, string $thumbnailExtension): string
|
||||||
|
{
|
||||||
|
// If any of the required fields are empty, return an empty string.
|
||||||
|
if (empty($fileID) || empty($thumbnailHash) || empty($thumbnailExtension)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build some paths/URLs using the mod data.
|
||||||
|
$hashShort = substr($thumbnailHash, 0, 2);
|
||||||
|
$fileName = $fileID.'.'.$thumbnailExtension;
|
||||||
|
$hubUrl = 'https://hub.sp-tarkov.com/files/images/file/'.$hashShort.'/'.$fileName;
|
||||||
|
$relativePath = 'mods/'.$fileName;
|
||||||
|
|
||||||
|
// Determine the disk to use based on the environment.
|
||||||
|
$disk = match (config('app.env')) {
|
||||||
|
'production' => 'r2', // Cloudflare R2 Storage
|
||||||
|
default => 'public', // Local
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check to make sure the image doesn't already exist.
|
||||||
|
if (Storage::disk($disk)->exists($relativePath)) {
|
||||||
|
return $relativePath; // Already exists, return the path.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the image using the cURL handler.
|
||||||
|
curl_setopt($curl, CURLOPT_URL, $hubUrl);
|
||||||
|
$image = curl_exec($curl);
|
||||||
|
|
||||||
|
if ($image === false) {
|
||||||
|
Log::error('There was an error attempting to download a mod thumbnail. cURL error: '.curl_error($curl));
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the image on the disk.
|
||||||
|
Storage::disk($disk)->put($relativePath, $image);
|
||||||
|
|
||||||
|
return $relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import the mod versions from the Hub database to the local database.
|
||||||
|
*/
|
||||||
|
protected function importModVersions(): void
|
||||||
|
{
|
||||||
|
DB::connection('mysql_hub')
|
||||||
|
->table('filebase1_file_version')
|
||||||
|
->chunkById(500, function (Collection $versions) {
|
||||||
|
|
||||||
|
foreach ($versions as $version) {
|
||||||
|
$versionContent = DB::table('temp_file_version_content')
|
||||||
|
->select('description')
|
||||||
|
->where('versionID', $version->versionID)
|
||||||
|
->orderBy('versionID', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$optionVirusTotal = DB::table('temp_file_option_values')
|
||||||
|
->select('optionValue as virus_total_link')
|
||||||
|
->where('fileID', $version->fileID)
|
||||||
|
->whereIn('optionID', [6, 2])
|
||||||
|
->whereNot('optionValue', '')
|
||||||
|
->orderByDesc('optionID')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$versionLabel = DB::table('temp_file_version_labels')
|
||||||
|
->select('labelID')
|
||||||
|
->where('objectID', $version->fileID)
|
||||||
|
->orderBy('labelID', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$modId = Mod::whereHubId($version->fileID)->value('id');
|
||||||
|
|
||||||
|
// Skip the mod version if it doesn't have a mod or version label attached to it.
|
||||||
|
if (empty($versionLabel) || empty($modId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$insertData[] = [
|
||||||
|
'hub_id' => (int) $version->versionID,
|
||||||
|
'mod_id' => $modId,
|
||||||
|
'version' => $version->versionNumber,
|
||||||
|
'description' => $this->cleanHubContent($versionContent->description ?? ''),
|
||||||
|
'link' => $version->downloadURL,
|
||||||
|
'spt_version_id' => SptVersion::whereHubId($versionLabel->labelID)->value('id'),
|
||||||
|
'virus_total_link' => $optionVirusTotal?->virus_total_link ?? '',
|
||||||
|
'downloads' => max((int) $version->downloads, 0), // At least 0.
|
||||||
|
'disabled' => (bool) $version->isDisabled,
|
||||||
|
'created_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||||
|
'updated_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($insertData)) {
|
||||||
|
ModVersion::upsert($insertData, ['hub_id'], [
|
||||||
|
'mod_id',
|
||||||
|
'version',
|
||||||
|
'description',
|
||||||
|
'link',
|
||||||
|
'spt_version_id',
|
||||||
|
'virus_total_link',
|
||||||
|
'downloads',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}, 'versionID');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The job failed to process.
|
||||||
|
*/
|
||||||
|
public function failed(Exception $exception): void
|
||||||
|
{
|
||||||
|
// Explicitly drop the temporary tables.
|
||||||
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
|
||||||
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
||||||
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_version_labels');
|
||||||
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_version_content');
|
||||||
|
|
||||||
|
// Close the connections. This should drop the temporary tables as well, but I like to be explicit.
|
||||||
|
DB::connection('mysql_hub')->disconnect();
|
||||||
|
DB::disconnect();
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
@ -11,9 +12,11 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
|
|||||||
use Laravel\Jetstream\HasProfilePhoto;
|
use Laravel\Jetstream\HasProfilePhoto;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
use Laravel\Scout\Searchable;
|
use Laravel\Scout\Searchable;
|
||||||
|
use Mchev\Banhammer\Traits\Bannable;
|
||||||
|
|
||||||
class User extends Authenticatable implements MustVerifyEmail
|
class User extends Authenticatable implements MustVerifyEmail
|
||||||
{
|
{
|
||||||
|
use Bannable;
|
||||||
use HasApiTokens;
|
use HasApiTokens;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use HasProfilePhoto;
|
use HasProfilePhoto;
|
||||||
@ -38,14 +41,6 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
'profile_photo_url',
|
'profile_photo_url',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'email_verified_at' => 'datetime',
|
|
||||||
'password' => 'hashed',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mods(): HasMany
|
public function mods(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Mod::class);
|
return $this->hasMany(Mod::class);
|
||||||
@ -63,4 +58,24 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
{
|
{
|
||||||
return ! is_null($this->email_verified_at);
|
return ! is_null($this->email_verified_at);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function assignRole(UserRole $role): bool
|
||||||
|
{
|
||||||
|
$this->role()->associate($role);
|
||||||
|
|
||||||
|
return $this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function role(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(UserRole::class, 'user_role_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
24
app/Models/UserRole.php
Normal file
24
app/Models/UserRole.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class UserRole extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'short_name',
|
||||||
|
'description',
|
||||||
|
'color_class',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function users(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(User::class);
|
||||||
|
}
|
||||||
|
}
|
54
app/Nova/UserRoleResource.php
Normal file
54
app/Nova/UserRoleResource.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Nova;
|
||||||
|
|
||||||
|
use App\Models\UserRole;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Laravel\Nova\Fields\ID;
|
||||||
|
use Laravel\Nova\Fields\Text;
|
||||||
|
|
||||||
|
class UserRoleResource extends Resource
|
||||||
|
{
|
||||||
|
public static string $model = UserRole::class;
|
||||||
|
|
||||||
|
public static $title = 'name';
|
||||||
|
|
||||||
|
public static $search = [
|
||||||
|
'id', 'name', 'description',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function fields(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ID::make()->sortable(),
|
||||||
|
|
||||||
|
Text::make('Name')
|
||||||
|
->sortable()
|
||||||
|
->rules('required'),
|
||||||
|
|
||||||
|
Text::make('Description')
|
||||||
|
->sortable()
|
||||||
|
->rules('required'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cards(Request $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filters(Request $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lenses(Request $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function actions(Request $request): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
36
app/Providers/HorizonServiceProvider.php
Normal file
36
app/Providers/HorizonServiceProvider.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Laravel\Horizon\Horizon;
|
||||||
|
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
||||||
|
|
||||||
|
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
// Horizon::routeSmsNotificationsTo('15556667777');
|
||||||
|
// Horizon::routeMailNotificationsTo('example@example.com');
|
||||||
|
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the Horizon gate.
|
||||||
|
*
|
||||||
|
* This gate determines who can access Horizon in non-local environments.
|
||||||
|
*/
|
||||||
|
protected function gate(): void
|
||||||
|
{
|
||||||
|
Gate::define('viewHorizon', function ($user) {
|
||||||
|
return in_array($user->email, [
|
||||||
|
//
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
use Mchev\Banhammer\Middleware\IPBanned;
|
||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
@ -12,7 +13,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
//
|
$middleware->append(IPBanned::class);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
//
|
//
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
return [
|
return [
|
||||||
App\Providers\AppServiceProvider::class,
|
App\Providers\AppServiceProvider::class,
|
||||||
App\Providers\FortifyServiceProvider::class,
|
App\Providers\FortifyServiceProvider::class,
|
||||||
|
App\Providers\HorizonServiceProvider::class,
|
||||||
App\Providers\JetstreamServiceProvider::class,
|
App\Providers\JetstreamServiceProvider::class,
|
||||||
App\Providers\NovaServiceProvider::class,
|
App\Providers\NovaServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"ext-curl": "*",
|
"ext-curl": "*",
|
||||||
"http-interop/http-factory-guzzle": "^1.2",
|
"http-interop/http-factory-guzzle": "^1.2",
|
||||||
"laravel/framework": "^11.0",
|
"laravel/framework": "^11.0",
|
||||||
|
"laravel/horizon": "^5.24",
|
||||||
"laravel/jetstream": "^5.1",
|
"laravel/jetstream": "^5.1",
|
||||||
"laravel/nova": "^4.0",
|
"laravel/nova": "^4.0",
|
||||||
"laravel/pulse": "^1.1",
|
"laravel/pulse": "^1.1",
|
||||||
@ -21,6 +22,7 @@
|
|||||||
"league/flysystem-aws-s3-v3": "3.0",
|
"league/flysystem-aws-s3-v3": "3.0",
|
||||||
"league/html-to-markdown": "^5.1",
|
"league/html-to-markdown": "^5.1",
|
||||||
"livewire/livewire": "^3.0",
|
"livewire/livewire": "^3.0",
|
||||||
|
"mchev/banhammer": "^2.3",
|
||||||
"meilisearch/meilisearch-php": "^1.8",
|
"meilisearch/meilisearch-php": "^1.8",
|
||||||
"stevebauman/purify": "^6.2"
|
"stevebauman/purify": "^6.2"
|
||||||
},
|
},
|
||||||
@ -29,6 +31,7 @@
|
|||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
"larastan/larastan": "^2.9",
|
"larastan/larastan": "^2.9",
|
||||||
"laravel/pint": "^1.16",
|
"laravel/pint": "^1.16",
|
||||||
|
"laravel/sail": "^1.29",
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "^8.0",
|
"nunomaduro/collision": "^8.0",
|
||||||
"phpunit/phpunit": "^11.0.1",
|
"phpunit/phpunit": "^11.0.1",
|
||||||
@ -81,6 +84,10 @@
|
|||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
"pestphp/pest-plugin": true,
|
"pestphp/pest-plugin": true,
|
||||||
"php-http/discovery": true
|
"php-http/discovery": true
|
||||||
|
},
|
||||||
|
"platform": {
|
||||||
|
"ext-pcntl": "8.0",
|
||||||
|
"ext-posix": "8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
|
793
composer.lock
generated
793
composer.lock
generated
File diff suppressed because it is too large
Load Diff
95
config/ban.php
Normal file
95
config/ban.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Table Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Specify the name of the table created during migration for the ban system.
|
||||||
|
| This table will store information about banned users.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'table' => 'bans',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Model Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Specify the model which you want to use as your Ban model.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model' => \Mchev\Banhammer\Models\Ban::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Where to Redirect Banned Users
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Define the URL to which users will be redirected when attempting to log in
|
||||||
|
| after being banned. If not defined, the banned user will be redirected to
|
||||||
|
| the previous page they tried to access.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'fallback_url' => null, // Examples: null (default), "/oops", "/login"
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 403 Message
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The message that will be displayed if no fallback URL is defined for banned users.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'messages' => [
|
||||||
|
'user' => 'Your account has been banned.',
|
||||||
|
'ip' => 'Access from your IP address is restricted.',
|
||||||
|
'country' => 'Access from your country is restricted.',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Block by Country
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Determine whether to block users based on their country. This setting uses
|
||||||
|
| the value of BANHAMMER_BLOCK_BY_COUNTRY from the environment. Enabling this
|
||||||
|
| feature may result in up to 45 HTTP requests per minute with the free version
|
||||||
|
| of https://ip-api.com/.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'block_by_country' => env('BANHAMMER_BLOCK_BY_COUNTRY', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| List of Blocked Countries
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Specify the countries where users will be blocked if 'block_by_country' is true.
|
||||||
|
| Add country codes to the array to restrict access from those countries.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'blocked_countries' => [], // Examples: ['US', 'CA', 'GB', 'FR', 'ES', 'DE']
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Duration for IP Geolocation
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This configuration option determines the duration, in minutes, for which
|
||||||
|
| the IP geolocation data will be stored in the cache. This helps prevent
|
||||||
|
| excessive requests and enables the middleware to efficiently determine
|
||||||
|
| whether to block a request based on the user's country.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'cache_duration' => 120, // Duration in minutes
|
||||||
|
|
||||||
|
];
|
@ -190,7 +190,7 @@ return [
|
|||||||
'username' => env('REDIS_USERNAME'),
|
'username' => env('REDIS_USERNAME'),
|
||||||
'password' => env('REDIS_PASSWORD'),
|
'password' => env('REDIS_PASSWORD'),
|
||||||
'port' => env('REDIS_PORT', '6379'),
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
'database' => env('REDIS_CACHE_DB', '2'),
|
'database' => env('REDIS_QUEUE_DB', '2'),
|
||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
230
config/horizon.php
Normal file
230
config/horizon.php
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Horizon Domain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the subdomain where Horizon will be accessible from. If this
|
||||||
|
| setting is null, Horizon will reside under the same domain as the
|
||||||
|
| application. Otherwise, this value will serve as the subdomain.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'domain' => env('HORIZON_DOMAIN'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Horizon Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the URI path where Horizon will be accessible from. Feel free
|
||||||
|
| to change this path to anything you like. Note that the URI will not
|
||||||
|
| affect the paths of its internal API that aren't exposed to users.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'path' => env('HORIZON_PATH', 'horizon'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Horizon Redis Connection
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the name of the Redis connection where Horizon will store the
|
||||||
|
| meta information required for it to function. It includes the list
|
||||||
|
| of supervisors, failed jobs, job metrics, and other information.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use' => 'default',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Horizon Redis Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This prefix will be used when storing all Horizon data in Redis. You
|
||||||
|
| may modify the prefix when you are running multiple installations
|
||||||
|
| of Horizon on the same server so that they don't have problems.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => env(
|
||||||
|
'HORIZON_PREFIX',
|
||||||
|
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'
|
||||||
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Horizon Route Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These middleware will get attached onto each Horizon route, giving you
|
||||||
|
| the chance to add your own middleware to this list or change any of
|
||||||
|
| the existing middleware. Or, you can simply stick with this list.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => ['web'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queue Wait Time Thresholds
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option allows you to configure when the LongWaitDetected event
|
||||||
|
| will be fired. Every connection / queue combination may have its
|
||||||
|
| own, unique threshold (in seconds) before this event is fired.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'waits' => [
|
||||||
|
'redis:default' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Job Trimming Times
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you can configure for how long (in minutes) you desire Horizon to
|
||||||
|
| persist the recent and failed jobs. Typically, recent jobs are kept
|
||||||
|
| for one hour while all failed jobs are stored for an entire week.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'trim' => [
|
||||||
|
'recent' => 60,
|
||||||
|
'pending' => 60,
|
||||||
|
'completed' => 60,
|
||||||
|
'recent_failed' => 10080,
|
||||||
|
'failed' => 10080,
|
||||||
|
'monitored' => 10080,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Silenced Jobs
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Silencing a job will instruct Horizon to not place the job in the list
|
||||||
|
| of completed jobs within the Horizon dashboard. This setting may be
|
||||||
|
| used to fully remove any noisy jobs from the completed jobs list.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'silenced' => [
|
||||||
|
// App\Jobs\ExampleJob::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Metrics
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you can configure how many snapshots should be kept to display in
|
||||||
|
| the metrics graph. This will get used in combination with Horizon's
|
||||||
|
| `horizon:snapshot` schedule to define how long to retain metrics.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'metrics' => [
|
||||||
|
'trim_snapshots' => [
|
||||||
|
'job' => 24,
|
||||||
|
'queue' => 24,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fast Termination
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When this option is enabled, Horizon's "terminate" command will not
|
||||||
|
| wait on all of the workers to terminate unless the --wait option
|
||||||
|
| is provided. Fast termination can shorten deployment delay by
|
||||||
|
| allowing a new instance of Horizon to start while the last
|
||||||
|
| instance will continue to terminate each of its workers.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'fast_termination' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Memory Limit (MB)
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value describes the maximum amount of memory the Horizon master
|
||||||
|
| supervisor may consume before it is terminated and restarted. For
|
||||||
|
| configuring these limits on your workers, see the next section.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'memory_limit' => 64,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queue Worker Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the queue worker settings used by your application
|
||||||
|
| in all environments. These supervisors and settings handle all your
|
||||||
|
| queued jobs and will be provisioned by Horizon during deployment.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'supervisor-1' => [
|
||||||
|
'connection' => 'redis',
|
||||||
|
'queue' => ['default'],
|
||||||
|
'balance' => 'auto',
|
||||||
|
'balanceMaxShift' => 1,
|
||||||
|
'balanceCooldown' => 3,
|
||||||
|
'autoScalingStrategy' => 'time',
|
||||||
|
'maxProcesses' => 1,
|
||||||
|
'maxTime' => 0,
|
||||||
|
'maxJobs' => 0,
|
||||||
|
'memory' => 128,
|
||||||
|
'tries' => 1,
|
||||||
|
'timeout' => 60,
|
||||||
|
'nice' => 0,
|
||||||
|
],
|
||||||
|
'supervisor-long' => [
|
||||||
|
'connection' => 'redis',
|
||||||
|
'queue' => ['long'],
|
||||||
|
'balance' => 'auto',
|
||||||
|
'balanceMaxShift' => 1,
|
||||||
|
'balanceCooldown' => 3,
|
||||||
|
'autoScalingStrategy' => 'time',
|
||||||
|
'maxProcesses' => 1,
|
||||||
|
'maxTime' => 0,
|
||||||
|
'maxJobs' => 0,
|
||||||
|
'memory' => 256,
|
||||||
|
'tries' => 1,
|
||||||
|
'timeout' => 900, // 15 Minutes
|
||||||
|
'nice' => 0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'environments' => [
|
||||||
|
'production' => [
|
||||||
|
'supervisor-1' => [
|
||||||
|
'maxProcesses' => 12,
|
||||||
|
],
|
||||||
|
'supervisor-long' => [
|
||||||
|
'maxProcesses' => 4,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'local' => [
|
||||||
|
'supervisor-1' => [],
|
||||||
|
'supervisor-long' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
@ -65,7 +65,7 @@ return [
|
|||||||
|
|
||||||
'redis' => [
|
'redis' => [
|
||||||
'driver' => 'redis',
|
'driver' => 'redis',
|
||||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
'connection' => env('REDIS_QUEUE_CONNECTION', 'queue'),
|
||||||
'queue' => env('REDIS_QUEUE', 'default'),
|
'queue' => env('REDIS_QUEUE', 'default'),
|
||||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||||
'block_for' => null,
|
'block_for' => null,
|
||||||
|
@ -2,16 +2,10 @@
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
use App\Models\Team;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Jetstream\Features;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
|
||||||
*/
|
|
||||||
class UserFactory extends Factory
|
class UserFactory extends Factory
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@ -20,9 +14,7 @@ class UserFactory extends Factory
|
|||||||
protected static ?string $password;
|
protected static ?string $password;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the model's default state.
|
* Define the user's default state.
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
*/
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
@ -34,13 +26,14 @@ class UserFactory extends Factory
|
|||||||
'two_factor_secret' => null,
|
'two_factor_secret' => null,
|
||||||
'two_factor_recovery_codes' => null,
|
'two_factor_recovery_codes' => null,
|
||||||
'remember_token' => Str::random(10),
|
'remember_token' => Str::random(10),
|
||||||
|
'user_role_id' => null,
|
||||||
'profile_photo_path' => null,
|
'profile_photo_path' => null,
|
||||||
'current_team_id' => null,
|
'current_team_id' => null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate that the model's email address should be unverified.
|
* Indicate that the user's email address should be unverified.
|
||||||
*/
|
*/
|
||||||
public function unverified(): static
|
public function unverified(): static
|
||||||
{
|
{
|
||||||
@ -48,25 +41,4 @@ class UserFactory extends Factory
|
|||||||
'email_verified_at' => null,
|
'email_verified_at' => null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicate that the user should have a personal team.
|
|
||||||
*/
|
|
||||||
public function withPersonalTeam(?callable $callback = null): static
|
|
||||||
{
|
|
||||||
if (! Features::hasTeamFeatures()) {
|
|
||||||
return $this->state([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->has(
|
|
||||||
Team::factory()
|
|
||||||
->state(fn (array $attributes, User $user) => [
|
|
||||||
'name' => $user->name.'\'s Team',
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'personal_team' => true,
|
|
||||||
])
|
|
||||||
->when(is_callable($callback), $callback),
|
|
||||||
'ownedTeams'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
50
database/factories/UserRoleFactory.php
Normal file
50
database/factories/UserRoleFactory.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\UserRole;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
class UserRoleFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = UserRole::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $this->faker->name(),
|
||||||
|
'short_name' => $this->faker->word(),
|
||||||
|
'description' => $this->faker->text(),
|
||||||
|
'color_class' => $this->faker->randomElement(['sky', 'red', 'green', 'emerald', 'lime']),
|
||||||
|
'created_at' => Carbon::now(),
|
||||||
|
'updated_at' => Carbon::now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the "administrator" role.
|
||||||
|
*/
|
||||||
|
public function administrator(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'name' => 'Administrator',
|
||||||
|
'short_name' => 'Admin',
|
||||||
|
'description' => 'An administrator has full access to the site.',
|
||||||
|
'color_class' => 'sky',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the "moderator" role.
|
||||||
|
*/
|
||||||
|
public function moderator(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'name' => 'Moderator',
|
||||||
|
'short_name' => 'Mod',
|
||||||
|
'description' => 'A moderator has the ability to moderate user content.',
|
||||||
|
'color_class' => 'emerald',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
38
database/migrations/2024_06_10_002858_create_mod_indexes.php
Normal file
38
database/migrations/2024_06_10_002858_create_mod_indexes.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('mods', function (Blueprint $table) {
|
||||||
|
$table->index(['deleted_at', 'disabled'], 'mods_show_index');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('mod_versions', function (Blueprint $table) {
|
||||||
|
$table->index(['mod_id', 'deleted_at', 'disabled', 'version'], 'mod_versions_filtering_index');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('spt_versions', function (Blueprint $table) {
|
||||||
|
$table->index(['version', 'deleted_at'], 'spt_versions_filtering_index');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('mods', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('mods_show_index');
|
||||||
|
$table->dropIndex('mod_versions_filtering_index');
|
||||||
|
$table->dropIndex('spt_versions_filtering_index');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
35
database/migrations/2024_06_15_000000_create_bans_table.php
Normal file
35
database/migrations/2024_06_15_000000_create_bans_table.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create(config('ban.table'), function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->nullableMorphs('bannable');
|
||||||
|
$table->nullableMorphs('created_by');
|
||||||
|
$table->text('comment')->nullable();
|
||||||
|
$table->string('ip', 45)->nullable();
|
||||||
|
$table->timestamp('expired_at')->nullable();
|
||||||
|
$table->softDeletes();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index('ip');
|
||||||
|
$table->index('expired_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists(config('ban.table'));
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasColumn(config('ban.table'), 'metas')) {
|
||||||
|
Schema::table(config('ban.table'), function (Blueprint $table) {
|
||||||
|
$table->json('metas')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (Schema::hasColumn(config('ban.table'), 'metas')) {
|
||||||
|
Schema::table(config('ban.table'), function (Blueprint $table) {
|
||||||
|
$table->dropColumn('metas');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('user_roles', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name')->unique();
|
||||||
|
$table->string('short_name')->default('');
|
||||||
|
$table->string('description')->default('');
|
||||||
|
$table->string('color_class')->default('');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('user_roles');
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\UserRole;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->foreignIdFor(UserRole::class)
|
||||||
|
->nullable()
|
||||||
|
->after('remember_token')
|
||||||
|
->constrained()
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['user_role_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -7,6 +7,7 @@ use App\Models\Mod;
|
|||||||
use App\Models\ModVersion;
|
use App\Models\ModVersion;
|
||||||
use App\Models\SptVersion;
|
use App\Models\SptVersion;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\UserRole;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
class DatabaseSeeder extends Seeder
|
||||||
@ -22,6 +23,14 @@ class DatabaseSeeder extends Seeder
|
|||||||
// Create some code licenses.
|
// Create some code licenses.
|
||||||
$licenses = License::factory(10)->create();
|
$licenses = License::factory(10)->create();
|
||||||
|
|
||||||
|
// Add 5 administrators.
|
||||||
|
$administrator = UserRole::factory()->administrator()->create();
|
||||||
|
User::factory(5)->create(['user_role_id' => $administrator->id]);
|
||||||
|
|
||||||
|
// Add 10 moderators.
|
||||||
|
$moderator = UserRole::factory()->moderator()->create();
|
||||||
|
User::factory(10)->create(['user_role_id' => $moderator->id]);
|
||||||
|
|
||||||
// Add 100 users.
|
// Add 100 users.
|
||||||
$users = User::factory(100)->create();
|
$users = User::factory(100)->create();
|
||||||
|
|
||||||
|
103
docker-compose.yml
Normal file
103
docker-compose.yml
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
services:
|
||||||
|
laravel.test:
|
||||||
|
build:
|
||||||
|
context: ./vendor/laravel/sail/runtimes/8.3
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
WWWGROUP: '${WWWGROUP}'
|
||||||
|
image: sail-8.3/app
|
||||||
|
extra_hosts:
|
||||||
|
- 'host.docker.internal:host-gateway'
|
||||||
|
ports:
|
||||||
|
- '${APP_PORT:-80}:80'
|
||||||
|
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
|
||||||
|
environment:
|
||||||
|
WWWUSER: '${WWWUSER}'
|
||||||
|
LARAVEL_SAIL: 1
|
||||||
|
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
|
||||||
|
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
|
||||||
|
IGNITION_LOCAL_SITES_PATH: '${PWD}'
|
||||||
|
volumes:
|
||||||
|
- '.:/var/www/html'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
- redis
|
||||||
|
- meilisearch
|
||||||
|
- mailpit
|
||||||
|
mysql:
|
||||||
|
image: 'mysql/mysql-server:8.0'
|
||||||
|
ports:
|
||||||
|
- '${FORWARD_DB_PORT:-3306}:3306'
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
|
||||||
|
MYSQL_ROOT_HOST: '%'
|
||||||
|
MYSQL_DATABASE: '${DB_DATABASE}'
|
||||||
|
MYSQL_USER: '${DB_USERNAME}'
|
||||||
|
MYSQL_PASSWORD: '${DB_PASSWORD}'
|
||||||
|
MYSQL_ALLOW_EMPTY_PASSWORD: 1
|
||||||
|
volumes:
|
||||||
|
- 'sail-mysql:/var/lib/mysql'
|
||||||
|
- './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- mysqladmin
|
||||||
|
- ping
|
||||||
|
- '-p${DB_PASSWORD}'
|
||||||
|
retries: 3
|
||||||
|
timeout: 5s
|
||||||
|
redis:
|
||||||
|
image: 'redis:alpine'
|
||||||
|
ports:
|
||||||
|
- '${FORWARD_REDIS_PORT:-6379}:6379'
|
||||||
|
volumes:
|
||||||
|
- 'sail-redis:/data'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- redis-cli
|
||||||
|
- ping
|
||||||
|
retries: 3
|
||||||
|
timeout: 5s
|
||||||
|
meilisearch:
|
||||||
|
image: 'getmeili/meilisearch:latest'
|
||||||
|
ports:
|
||||||
|
- '${FORWARD_MEILISEARCH_PORT:-7700}:7700'
|
||||||
|
environment:
|
||||||
|
MEILI_NO_ANALYTICS: '${MEILISEARCH_NO_ANALYTICS:-false}'
|
||||||
|
volumes:
|
||||||
|
- 'sail-meilisearch:/meili_data'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- wget
|
||||||
|
- '--no-verbose'
|
||||||
|
- '--spider'
|
||||||
|
- 'http://localhost:7700/health'
|
||||||
|
retries: 3
|
||||||
|
timeout: 5s
|
||||||
|
mailpit:
|
||||||
|
image: 'axllent/mailpit:latest'
|
||||||
|
ports:
|
||||||
|
- '${FORWARD_MAILPIT_PORT:-1025}:1025'
|
||||||
|
- '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
networks:
|
||||||
|
sail:
|
||||||
|
driver: bridge
|
||||||
|
volumes:
|
||||||
|
sail-mysql:
|
||||||
|
driver: local
|
||||||
|
sail-redis:
|
||||||
|
driver: local
|
||||||
|
sail-meilisearch:
|
||||||
|
driver: local
|
@ -22,7 +22,7 @@
|
|||||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="CACHE_STORE" value="array"/>
|
<env name="CACHE_STORE" value="array"/>
|
||||||
<env name="DB_DATABASE" value="test"/>
|
<env name="DB_DATABASE" value="testing"/>
|
||||||
<env name="MAIL_MAILER" value="array"/>
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
<env name="PULSE_ENABLED" value="false"/>
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
2
public/vendor/nova/app.js
vendored
2
public/vendor/nova/app.js
vendored
File diff suppressed because one or more lines are too long
2
public/vendor/nova/app.js.map
vendored
2
public/vendor/nova/app.js.map
vendored
File diff suppressed because one or more lines are too long
2
public/vendor/nova/mix-manifest.json
vendored
2
public/vendor/nova/mix-manifest.json
vendored
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"/app.js": "/app.js?id=d5c07e2eeefdadf9650a9473ea340c89",
|
"/app.js": "/app.js?id=4f1ec3789b86ed89a9a1900e986b95e6",
|
||||||
"/manifest.js": "/manifest.js?id=d6d76d12b7219df564489d400c711198",
|
"/manifest.js": "/manifest.js?id=d6d76d12b7219df564489d400c711198",
|
||||||
"/app.css": "/app.css?id=3d962b859bf103c1663adb9513497f17",
|
"/app.css": "/app.css?id=3d962b859bf103c1663adb9513497f17",
|
||||||
"/vendor.js": "/vendor.js?id=0b026297072f6c8be97d0c900a2d4770",
|
"/vendor.js": "/vendor.js?id=0b026297072f6c8be97d0c900a2d4770",
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<img src="https://placehold.co/450x450/EEE/31343C?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="block dark:hidden h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
<img src="https://placehold.co/450x450/EEE/31343C?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="block dark:hidden h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
||||||
<img src="https://placehold.co/450x450/31343C/EEE?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="hidden dark:block h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
<img src="https://placehold.co/450x450/31343C/EEE?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="hidden dark:block h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
||||||
@else
|
@else
|
||||||
<img src="{{ Storage::url($mod->thumbnail) }}" alt="{{ $mod->name }}" class="h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
<img src="{{ asset($mod->thumbnail) }}" alt="{{ $mod->name }}" class="h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col w-full justify-between p-5">
|
<div class="flex flex-col w-full justify-between p-5">
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
<link href="//fonts.bunny.net" rel="preconnect">
|
<link href="//fonts.bunny.net" rel="preconnect">
|
||||||
<link href="//fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet">
|
<link href="//fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<link href="{{ config('app.asset_url') }}" rel="dns-prefetch">
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Immediately set the theme to prevent a flash of the default theme when another is set.
|
// Immediately set the theme to prevent a flash of the default theme when another is set.
|
||||||
// Must be located inline, in the head, and before any CSS is loaded.
|
// Must be located inline, in the head, and before any CSS is loaded.
|
||||||
|
@ -12,7 +12,14 @@
|
|||||||
|
|
||||||
{{-- Navigation Links --}}
|
{{-- Navigation Links --}}
|
||||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||||
{{-- <x-nav-link href="{{ route('mods') }}" :active="request()->routeIs('mods')">{{ __('Mods') }}</x-nav-link> --}}
|
@auth
|
||||||
|
<x-nav-link href="#">{{ __('Dashboard') }}</x-nav-link>
|
||||||
|
@endauth
|
||||||
|
<x-nav-link href="#">{{ __('About') }}</x-nav-link>
|
||||||
|
<x-nav-link href="#">{{ __('Articles') }}</x-nav-link>
|
||||||
|
<x-nav-link href="#">{{ __('Documentation') }}</x-nav-link>
|
||||||
|
<x-nav-link href="{{ route('mods') }}" :active="request()->routeIs('mods')">{{ __('Mods') }}</x-nav-link>
|
||||||
|
<x-nav-link href="#">{{ __('Support') }}</x-nav-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -35,7 +42,7 @@
|
|||||||
|
|
||||||
@guest
|
@guest
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<a href="{{ route('login') }}" class="text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100">Log in
|
<a href="{{ route('login') }}" class="text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100 whitespace-nowrap">Log in
|
||||||
<span aria-hidden="true">→</span></a>
|
<span aria-hidden="true">→</span></a>
|
||||||
</div>
|
</div>
|
||||||
@endguest
|
@endguest
|
||||||
@ -100,14 +107,14 @@
|
|||||||
{{-- Responsive Navigation Menu --}}
|
{{-- Responsive Navigation Menu --}}
|
||||||
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||||
<div class="pt-2 pb-3 space-y-1">
|
<div class="pt-2 pb-3 space-y-1">
|
||||||
<x-responsive-nav-link href="{{ route('home') }}" :active="request()->routeIs('home')">
|
|
||||||
{{ __('Home') }}
|
|
||||||
</x-responsive-nav-link>
|
|
||||||
@auth()
|
@auth()
|
||||||
<x-responsive-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
|
<x-responsive-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">{{ __('Dashboard') }}</x-responsive-nav-link>
|
||||||
{{ __('Dashboard') }}
|
|
||||||
</x-responsive-nav-link>
|
|
||||||
@endauth
|
@endauth
|
||||||
|
<x-responsive-nav-link href="#">{{ __('About') }}</x-responsive-nav-link>
|
||||||
|
<x-responsive-nav-link href="#">{{ __('Articles') }}</x-responsive-nav-link>
|
||||||
|
<x-responsive-nav-link href="#">{{ __('Documentation') }}</x-responsive-nav-link>
|
||||||
|
<x-responsive-nav-link href="{{ route('mods') }}" :active="request()->routeIs('mods')">{{ __('Mods') }}</x-responsive-nav-link>
|
||||||
|
<x-responsive-nav-link href="#">{{ __('Support') }}</x-responsive-nav-link>
|
||||||
</div>
|
</div>
|
||||||
{{-- Responsive Settings Options --}}
|
{{-- Responsive Settings Options --}}
|
||||||
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-700">
|
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
@ -3,17 +3,21 @@
|
|||||||
use App\Http\Controllers\ModController;
|
use App\Http\Controllers\ModController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::middleware(['auth.banned'])->group(function () {
|
||||||
return view('home');
|
|
||||||
})->name('home');
|
|
||||||
|
|
||||||
Route::controller(ModController::class)->group(function () {
|
Route::get('/', function () {
|
||||||
Route::get('/mods', 'index')->name('mods');
|
return view('home');
|
||||||
Route::get('/mod/{mod}/{slug}', 'show')->where(['id' => '[0-9]+'])->name('mod.show');
|
})->name('home');
|
||||||
});
|
|
||||||
|
Route::controller(ModController::class)->group(function () {
|
||||||
|
Route::get('/mods', 'index')->name('mods');
|
||||||
|
Route::get('/mod/{mod}/{slug}', 'show')->where(['id' => '[0-9]+'])->name('mod.show');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])->group(function () {
|
||||||
|
Route::get('/dashboard', function () {
|
||||||
|
return view('dashboard');
|
||||||
|
})->name('dashboard');
|
||||||
|
});
|
||||||
|
|
||||||
Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])->group(function () {
|
|
||||||
Route::get('/dashboard', function () {
|
|
||||||
return view('dashboard');
|
|
||||||
})->name('dashboard');
|
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,7 @@ class ApiTokenPermissionsTest extends TestCase
|
|||||||
$this->markTestSkipped('API support is not enabled.');
|
$this->markTestSkipped('API support is not enabled.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->actingAs($user = User::factory()->withPersonalTeam()->create());
|
$this->actingAs($user = User::factory()->create());
|
||||||
|
|
||||||
$token = $user->tokens()->create([
|
$token = $user->tokens()->create([
|
||||||
'name' => 'Test Token',
|
'name' => 'Test Token',
|
||||||
|
@ -19,7 +19,7 @@ class CreateApiTokenTest extends TestCase
|
|||||||
$this->markTestSkipped('API support is not enabled.');
|
$this->markTestSkipped('API support is not enabled.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->actingAs($user = User::factory()->withPersonalTeam()->create());
|
$this->actingAs($user = User::factory()->create());
|
||||||
|
|
||||||
Livewire::test(ApiTokenManager::class)
|
Livewire::test(ApiTokenManager::class)
|
||||||
->set(['createApiTokenForm' => [
|
->set(['createApiTokenForm' => [
|
||||||
|
@ -20,7 +20,7 @@ class DeleteApiTokenTest extends TestCase
|
|||||||
$this->markTestSkipped('API support is not enabled.');
|
$this->markTestSkipped('API support is not enabled.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->actingAs($user = User::factory()->withPersonalTeam()->create());
|
$this->actingAs($user = User::factory()->create());
|
||||||
|
|
||||||
$token = $user->tokens()->create([
|
$token = $user->tokens()->create([
|
||||||
'name' => 'Test Token',
|
'name' => 'Test Token',
|
||||||
|
@ -20,7 +20,7 @@ class EmailVerificationTest extends TestCase
|
|||||||
$this->markTestSkipped('Email verification not enabled.');
|
$this->markTestSkipped('Email verification not enabled.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = User::factory()->withPersonalTeam()->unverified()->create();
|
$user = User::factory()->unverified()->create();
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get('/email/verify');
|
$response = $this->actingAs($user)->get('/email/verify');
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ class PasswordConfirmationTest extends TestCase
|
|||||||
|
|
||||||
public function test_confirm_password_screen_can_be_rendered(): void
|
public function test_confirm_password_screen_can_be_rendered(): void
|
||||||
{
|
{
|
||||||
$user = User::factory()->withPersonalTeam()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get('/user/confirm-password');
|
$response = $this->actingAs($user)->get('/user/confirm-password');
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user