mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-12 12:10:41 -05:00
Merge branch 'develop'
This commit is contained in:
commit
85c404bd5b
6
.env.ci
6
.env.ci
@ -9,13 +9,13 @@ LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=33306
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=testing
|
||||
DB_USERNAME=user
|
||||
DB_PASSWORD=password
|
||||
|
||||
SCOUT_DRIVER=null
|
||||
SCOUT_DRIVER=collection
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
CACHE_STORE=array
|
||||
|
@ -89,3 +89,11 @@ DB_HUB_COLLATION=utf8mb4_0900_ai_ci
|
||||
|
||||
GITEA_DOMAIN=
|
||||
GITEA_TOKEN=
|
||||
|
||||
# API key for Scribe documentation.
|
||||
SCRIBE_AUTH_KEY=
|
||||
|
||||
# Discord OAuth Credentials
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
DISCORD_REDIRECT_URI=${APP_URL}/login/discord/callback
|
||||
|
@ -42,3 +42,11 @@ SCOUT_DRIVER=collection
|
||||
MAIL_MAILER=log
|
||||
MAIL_FROM_ADDRESS="no-reply@sp-tarkov.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# API key for Scribe documentation.
|
||||
SCRIBE_AUTH_KEY=
|
||||
|
||||
# Discord OAuth Credentials
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
DISCORD_REDIRECT_URI=${APP_URL}/login/discord/callback
|
||||
|
@ -12,7 +12,7 @@ The Forge is a Laravel-based web application that provides a platform for the Si
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
We use [Laravel Sail](https://laravel.com/docs/11.x/sail) to mirror the services that are used in our production server in a local development environment. You can see detailed instructions on how to configure the [full development environment](https://github.com/sp-tarkov/forge/wiki/Full-Windows-Dev-Env) or a [lightweight development environment](https://github.com/sp-tarkov/forge/wiki/Light-Windows-Dev-Env) on the project wiki. The full development environment is recommended.
|
||||
We use [Laravel Sail](https://laravel.com/docs/11.x/sail) to mirror the services that are used in our production server in a local development environment. You can see detailed instructions on how to configure the [full development environment](https://dev.sp-tarkov.com/SPT/forge/wiki/Full-Windows-Dev-Env) or a [lightweight development environment](https://dev.sp-tarkov.com/SPT/forge/wiki/Light-Windows-Dev-Env) on the project wiki. The full development environment is recommended.
|
||||
|
||||
### Available Services:
|
||||
|
||||
@ -25,13 +25,13 @@ We use [Laravel Sail](https://laravel.com/docs/11.x/sail) to mirror the services
|
||||
|
||||
### Notable Routes
|
||||
|
||||
| Service | Authentication | Access Via Host |
|
||||
|----------------------------------|----------------|-----------------------------|
|
||||
| Service | Authentication | Access Via Host |
|
||||
|----------------------------------|----------------|----------------------------|
|
||||
| Laravel Filament Admin Panel | Via User Role | <http://localhost/admin> |
|
||||
| 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> |
|
||||
| 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.full` or `.env.light` example files. Simply save one of these (depending on your environment) as `.env` and adjust settings as needed.
|
||||
|
||||
@ -80,12 +80,12 @@ For more information on Laravel development, please refer to the [official docum
|
||||
|
||||
## 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.
|
||||
*__Please note__, we are very early in development and will likely not accept work that is not discussed beforehand.*
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
@ -4,7 +4,7 @@ on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
security-checker:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@ -12,66 +12,79 @@ jobs:
|
||||
uses: symfonycorp/security-checker-action@v5
|
||||
|
||||
larastan:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
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: 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 optimize
|
||||
|
||||
- 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
|
||||
runs-on: ubuntu-latest
|
||||
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: 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 optimize
|
||||
|
||||
- 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]
|
@ -4,7 +4,7 @@ on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
laravel-tests:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.3
|
||||
@ -13,53 +13,65 @@ jobs:
|
||||
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
|
||||
options: --health-cmd="mysql -u user -D testing -ppassword -h mysql -e ''" --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: 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 optimize
|
||||
|
||||
- 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
|
40
.github/dependabot.yml
vendored
40
.github/dependabot.yml
vendored
@ -1,40 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Composer dependencies (PHP)
|
||||
- package-ecosystem: "composer"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "15:00" # 10am EST
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "dependencies"
|
||||
assignees:
|
||||
- "Refringe"
|
||||
|
||||
# GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "15:00" # 10am EST
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "dependencies"
|
||||
assignees:
|
||||
- "Refringe"
|
||||
|
||||
# npm modules (JavaScript)
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "15:00" # 10am EST
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "dependencies"
|
||||
assignees:
|
||||
- "Refringe"
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -16,6 +16,10 @@ public/build
|
||||
public/hot
|
||||
public/storage
|
||||
storage/*.key
|
||||
storage/app/livewire-tmp
|
||||
storage/app/public
|
||||
!storage/app/public/cover-photos/.gitkeep
|
||||
!storage/app/public/profile-photos/.gitkeep
|
||||
vendor
|
||||
auth.json
|
||||
frankenphp
|
||||
@ -24,3 +28,4 @@ Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.scribe
|
||||
|
@ -20,7 +20,7 @@ class CreateNewUser implements CreatesNewUsers
|
||||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'name' => ['required', 'string', 'max:36', 'unique:users'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||
'password' => $this->passwordRules(),
|
||||
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
|
||||
|
@ -8,8 +8,6 @@ trait PasswordValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*
|
||||
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
|
@ -13,8 +13,6 @@ class ResetUserPassword implements ResetsUserPasswords
|
||||
|
||||
/**
|
||||
* Validate and reset the user's forgotten password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function reset(User $user, array $input): void
|
||||
{
|
||||
|
@ -13,8 +13,6 @@ class UpdateUserPassword implements UpdatesUserPasswords
|
||||
|
||||
/**
|
||||
* Validate and update the user's password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
|
@ -12,13 +12,11 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
{
|
||||
/**
|
||||
* Validate and update the given user's profile information.
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'name' => ['required', 'string', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
|
||||
'cover' => ['nullable', 'mimes:jpg,jpeg,png', 'max:2048'],
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\ImportHubDataJob;
|
||||
use App\Jobs\Import\ImportHubDataJob;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ImportHubCommand extends Command
|
||||
|
@ -9,41 +9,77 @@ use App\Traits\ApiResponses;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Knuckles\Scribe\Attributes\BodyParam;
|
||||
use Knuckles\Scribe\Attributes\Response;
|
||||
use Knuckles\Scribe\Attributes\ResponseField;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
use ApiResponses;
|
||||
|
||||
/**
|
||||
* Login
|
||||
*
|
||||
* Authenticates the user and returns a read-only API token. This API token can then be saved and used for future
|
||||
* requests that require authentication. <aside class="warning">This method is made available for mod authors to
|
||||
* incorporate into their mods so that users can easily authenticate using their own API token. For typical API use,
|
||||
* you should log into the website, create an API token, and use that token for your API requests.</aside>
|
||||
*
|
||||
* @unauthenticated
|
||||
*
|
||||
* @group Authentication
|
||||
*/
|
||||
#[BodyParam('token_name', 'string', 'The name of the API token.', required: false, example: 'Dynamic API Token')]
|
||||
#[Response(['message' => 'authenticated', 'data' => ['token' => 'YOUR_API_KEY'], 'status' => 200], status: 200, description: 'Authenticated successfully')]
|
||||
#[Response(['message' => 'invalid credentials', 'status' => 401], status: 401, description: 'Invalid credentials')]
|
||||
#[ResponseField('token', description: 'The newly created read-only API token to use for future authenticated requests.')]
|
||||
public function login(LoginUserRequest $request): JsonResponse
|
||||
{
|
||||
$request->validated($request->all());
|
||||
|
||||
if (! Auth::attempt($request->only('email', 'password'))) {
|
||||
return $this->error(__('Invalid credentials'), 401);
|
||||
return $this->error(__('invalid credentials'), 401);
|
||||
}
|
||||
|
||||
$user = User::firstWhere('email', $request->email);
|
||||
$tokenName = $request->token_name ?? __('Dynamic API Token');
|
||||
|
||||
return $this->success(__('Authenticated'), [
|
||||
return $this->success(__('authenticated'), [
|
||||
// Only allowing the 'read' scope to be dynamically created. Can revisit later when writes are possible.
|
||||
'token' => $user->createToken($tokenName, ['read'])->plainTextToken,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*
|
||||
* Destroys the user's current API token, effectively logging them out.
|
||||
*
|
||||
* @group Authentication
|
||||
*/
|
||||
#[Response(['message' => 'success', 'status' => 200], status: 200, description: 'Token destroyed successfully')]
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
/** @var \Laravel\Sanctum\PersonalAccessToken $token */
|
||||
/** @var PersonalAccessToken $token */
|
||||
$token = $request->user()->currentAccessToken();
|
||||
$token->delete();
|
||||
|
||||
return $this->success(__('Revoked API token'));
|
||||
return $this->success(__('success'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout All
|
||||
*
|
||||
* Destroys all the user's API tokens, effectively logging everyone out of the account.
|
||||
*
|
||||
* @group Authentication
|
||||
*/
|
||||
#[Response(['message' => 'success', 'status' => 200], status: 200, description: 'Tokens destroyed successfully')]
|
||||
public function logoutAll(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->tokens()->delete();
|
||||
|
||||
return $this->success(__('Revoked all API tokens'));
|
||||
return $this->success(__('success'));
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ namespace App\Http\Controllers\Api\V0;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Support\Str;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
class ApiController extends Controller
|
||||
{
|
||||
@ -13,7 +15,11 @@ class ApiController extends Controller
|
||||
*/
|
||||
public static function shouldInclude(string|array $relationships): bool
|
||||
{
|
||||
$param = request()->get('include');
|
||||
try {
|
||||
$param = request()->get('include');
|
||||
} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $param) {
|
||||
return false;
|
||||
|
@ -3,50 +3,52 @@
|
||||
namespace App\Http\Controllers\Api\V0;
|
||||
|
||||
use App\Http\Filters\V1\ModFilter;
|
||||
use App\Http\Requests\Api\V0\StoreModRequest;
|
||||
use App\Http\Requests\Api\V0\UpdateModRequest;
|
||||
use App\Http\Resources\Api\V0\ModResource;
|
||||
use App\Models\Mod;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Knuckles\Scribe\Attributes\QueryParam;
|
||||
use Knuckles\Scribe\Attributes\UrlParam;
|
||||
|
||||
class ModController extends ApiController
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
* Get Mods
|
||||
*
|
||||
* List, filter, and sort basic information about mods.
|
||||
*
|
||||
* @group Mods
|
||||
*/
|
||||
public function index(ModFilter $filters)
|
||||
#[QueryParam('include', 'string', 'The relationships to include within the `includes` key. By default no relationships are automatically included.', required: false, example: 'users,versions,license')]
|
||||
#[QueryParam('filter[id]', 'string', 'Filter by the `id`. Select multiple by separating the IDs with a comma.', required: false, example: '5,10,15')]
|
||||
#[QueryParam('filter[hub_id]', 'string', 'Filter by the `hub_id` attribute. Select multiple by separating the IDs with a comma.', required: false, example: '20')]
|
||||
#[QueryParam('filter[name]', 'string', 'Filter by the `name` attribute. Use `*` as the wildcard character.', required: false, example: '*SAIN*')]
|
||||
#[QueryParam('filter[slug]', 'string', 'Filter by the `slug` attribute. Use `*` as the wildcard character.', required: false, example: '*raid-times')]
|
||||
#[QueryParam('filter[teaser]', 'string', 'Filter by the `teaser` attribute. Use `*` as the wildcard character.', required: false, example: '*weighted*random*times*')]
|
||||
#[QueryParam('filter[source_code_link]', 'string', 'Filter by the `source_code_link` attribute. Use `*` as the wildcard character.', required: false, example: '*https*.net*')]
|
||||
#[QueryParam('filter[featured]', 'boolean', 'Filter by the `featured` attribute. All "truthy" or "falsy" values are supported.', required: false, example: 'true')]
|
||||
#[QueryParam('filter[contains_ads]', 'boolean', 'Filter by the `contains_ads` attribute. All "truthy" or "falsy" values are supported.', required: false, example: 'true')]
|
||||
#[QueryParam('filter[contains_ai_content]', 'boolean', 'Filter by the `contains_ai_content` attribute. All "truthy" or "falsy" values are supported.', required: false, example: 'true')]
|
||||
#[QueryParam('filter[created_at]', 'string', 'Filter by the `created_at` attribute. Ranges are possible by separating the dates with a comma.', required: false, example: '2023-12-31,2024-12-31')]
|
||||
#[QueryParam('filter[updated_at]', 'string', 'Filter by the `updated_at` attribute. Ranges are possible by separating the dates with a comma.', required: false, example: '2023-12-31,2024-12-31')]
|
||||
#[QueryParam('filter[published_at]', 'string', 'Filter by the `published_at` attribute. Ranges are possible by seperating the dates with a comma.', required: false, example: '2023-12-31,2024-12-31')]
|
||||
#[QueryParam('sort', 'string', 'Sort the results by a comma seperated list of attributes. The default sort direction is ASC, append the attribute name with a minus to sort DESC.', required: false, example: '-featured,name')]
|
||||
public function index(ModFilter $filters): AnonymousResourceCollection
|
||||
{
|
||||
return ModResource::collection(Mod::filter($filters)->paginate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
* Get Mod
|
||||
*
|
||||
* Display more detailed information about a specific mod.
|
||||
*
|
||||
* @group Mods
|
||||
*/
|
||||
public function store(StoreModRequest $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Mod $mod)
|
||||
#[UrlParam('id', 'integer', 'The ID of the mod.', required: true, example: 558)]
|
||||
#[QueryParam('include', 'string', 'The relationships to include within the `includes` key. By default no relationships are automatically included.', required: false, example: 'users,versions,license')]
|
||||
public function show(Mod $mod): JsonResource
|
||||
{
|
||||
return new ModResource($mod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(UpdateModRequest $request, Mod $mod)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Mod $mod)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
@ -3,50 +3,42 @@
|
||||
namespace App\Http\Controllers\Api\V0;
|
||||
|
||||
use App\Http\Filters\V1\UserFilter;
|
||||
use App\Http\Requests\Api\V0\StoreUserRequest;
|
||||
use App\Http\Requests\Api\V0\UpdateUserRequest;
|
||||
use App\Http\Resources\Api\V0\UserResource;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Knuckles\Scribe\Attributes\QueryParam;
|
||||
|
||||
class UsersController extends ApiController
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
* Get Users
|
||||
*
|
||||
* List, filter, and sort basic information about users.
|
||||
*
|
||||
* @group Users
|
||||
*/
|
||||
public function index(UserFilter $filters)
|
||||
#[QueryParam('include', 'string', 'The relationships to include within the `includes` key. By default no relationships are automatically included.', required: false, example: 'user_role')]
|
||||
#[QueryParam('filter[id]', 'string', 'Filter by the `id`. Select multiple by separating the IDs with a comma.', required: false, example: '5,10,15')]
|
||||
#[QueryParam('filter[name]', 'string', 'Filter by the `name` attribute. Use `*` as the wildcard character.', required: false, example: '*fringe')]
|
||||
#[QueryParam('filter[created_at]', 'string', 'Filter by the `created_at` attribute. Ranges are possible by separating the dates with a comma.', required: false, example: '2023-12-31,2024-12-31')]
|
||||
#[QueryParam('filter[updated_at]', 'string', 'Filter by the `updated_at` attribute. Ranges are possible by separating the dates with a comma.', required: false, example: '2023-12-31,2024-12-31')]
|
||||
#[QueryParam('sort', 'string', 'Sort the results by a comma seperated list of attributes. The default sort direction is ASC, append the attribute name with a minus to sort DESC.', required: false, example: 'created_at,-name')]
|
||||
public function index(UserFilter $filters): AnonymousResourceCollection
|
||||
{
|
||||
return UserResource::collection(User::filter($filters)->paginate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
* Get User
|
||||
*
|
||||
* Display more detailed information about a specific user.
|
||||
*
|
||||
* @group Users
|
||||
*/
|
||||
public function store(StoreUserRequest $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(User $user)
|
||||
#[QueryParam('include', 'string', 'The relationships to include within the `includes` key. By default no relationships are automatically included.', required: false, example: 'user_role')]
|
||||
public function show(User $user): JsonResource
|
||||
{
|
||||
return new UserResource($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(UpdateUserRequest $request, User $user)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(User $user)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
@ -6,37 +6,36 @@ use App\Http\Requests\ModRequest;
|
||||
use App\Http\Resources\ModResource;
|
||||
use App\Models\Mod;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ModController extends Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public function index()
|
||||
public function index(): View
|
||||
{
|
||||
$this->authorize('viewAny', Mod::class);
|
||||
|
||||
return view('mod.index');
|
||||
}
|
||||
|
||||
public function store(ModRequest $request)
|
||||
public function store(ModRequest $request): ModResource
|
||||
{
|
||||
$this->authorize('create', Mod::class);
|
||||
|
||||
return new ModResource(Mod::create($request->validated()));
|
||||
}
|
||||
|
||||
public function show(int $modId, string $slug)
|
||||
public function show(int $modId, string $slug): View
|
||||
{
|
||||
$mod = Mod::with([
|
||||
'versions',
|
||||
'versions.latestSptVersion:id,version,color_class',
|
||||
'versions.latestSptVersion',
|
||||
'versions.latestResolvedDependencies',
|
||||
'versions.latestResolvedDependencies.mod:id,name,slug',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->whereHas('latestVersion')
|
||||
->findOrFail($modId);
|
||||
'versions.latestResolvedDependencies.mod',
|
||||
'license',
|
||||
'users',
|
||||
])->findOrFail($modId);
|
||||
|
||||
if ($mod->slug !== $slug) {
|
||||
abort(404);
|
||||
@ -47,7 +46,7 @@ class ModController extends Controller
|
||||
return view('mod.show', compact(['mod']));
|
||||
}
|
||||
|
||||
public function update(ModRequest $request, Mod $mod)
|
||||
public function update(ModRequest $request, Mod $mod): ModResource
|
||||
{
|
||||
$this->authorize('update', $mod);
|
||||
|
||||
@ -56,12 +55,5 @@ class ModController extends Controller
|
||||
return new ModResource($mod);
|
||||
}
|
||||
|
||||
public function destroy(Mod $mod)
|
||||
{
|
||||
$this->authorize('delete', $mod);
|
||||
|
||||
$mod->delete();
|
||||
|
||||
return response()->json();
|
||||
}
|
||||
public function destroy(Mod $mod): void {}
|
||||
}
|
||||
|
42
app/Http/Controllers/ModVersionController.php
Normal file
42
app/Http/Controllers/ModVersionController.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ModVersion;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
||||
class ModVersionController extends Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public function show(Request $request, int $modId, string $slug, string $version): RedirectResponse
|
||||
{
|
||||
$modVersion = ModVersion::whereModId($modId)
|
||||
->whereVersion($version)
|
||||
->firstOrFail();
|
||||
|
||||
if ($modVersion->mod->slug !== $slug) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$this->authorize('view', $modVersion);
|
||||
|
||||
// Rate limit the downloads.
|
||||
$rateKey = 'mod-download:'.($request->user()?->id ?: $request->ip());
|
||||
if (RateLimiter::tooManyAttempts($rateKey, maxAttempts: 5)) { // Max attempts is per minute.
|
||||
abort(429);
|
||||
}
|
||||
|
||||
// Increment downloads counts in the background.
|
||||
defer(fn () => $modVersion->incrementDownloads());
|
||||
|
||||
// Increment the rate limiter.
|
||||
RateLimiter::increment($rateKey);
|
||||
|
||||
// Redirect to the download link, using a 307 status code to prevent browsers from caching.
|
||||
return redirect($modVersion->link, 307);
|
||||
}
|
||||
}
|
152
app/Http/Controllers/SocialiteController.php
Normal file
152
app/Http/Controllers/SocialiteController.php
Normal file
@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\OAuthConnection;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Socialite\Contracts\User as ProviderUser;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
|
||||
|
||||
class SocialiteController extends Controller
|
||||
{
|
||||
/**
|
||||
* The providers that are supported.
|
||||
*/
|
||||
protected array $providers = ['discord'];
|
||||
|
||||
/**
|
||||
* Redirect the user to the provider's authentication page.
|
||||
*/
|
||||
public function redirect(string $provider): SymfonyRedirectResponse
|
||||
{
|
||||
if (! in_array($provider, $this->providers)) {
|
||||
return redirect()->route('login')->withErrors(__('Unsupported OAuth provider.'));
|
||||
}
|
||||
|
||||
$socialiteProvider = Socialite::driver($provider);
|
||||
|
||||
if (method_exists($socialiteProvider, 'scopes')) {
|
||||
return $socialiteProvider->scopes(['identify', 'email'])->redirect();
|
||||
}
|
||||
|
||||
return $socialiteProvider->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the user information from the provider.
|
||||
*/
|
||||
public function callback(string $provider): RedirectResponse
|
||||
{
|
||||
if (! in_array($provider, $this->providers)) {
|
||||
return redirect()->route('login')->withErrors(__('Unsupported OAuth provider.'));
|
||||
}
|
||||
|
||||
try {
|
||||
$providerUser = Socialite::driver($provider)->user();
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->route('login')->withErrors('Unable to login using '.$provider.'. Please try again.');
|
||||
}
|
||||
|
||||
$user = $this->findOrCreateUser($provider, $providerUser);
|
||||
|
||||
Auth::login($user, remember: true);
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
protected function findOrCreateUser(string $provider, ProviderUser $providerUser): User
|
||||
{
|
||||
$oauthConnection = OAuthConnection::whereProvider($provider)
|
||||
->whereProviderId($providerUser->getId())
|
||||
->first();
|
||||
|
||||
if ($oauthConnection) {
|
||||
$oauthConnection->update([
|
||||
'token' => $providerUser->token ?? '',
|
||||
'refresh_token' => $providerUser->refreshToken ?? '',
|
||||
'nickname' => $providerUser->getNickname() ?? '',
|
||||
'name' => $providerUser->getName() ?? '',
|
||||
'email' => $providerUser->getEmail() ?? '',
|
||||
'avatar' => $providerUser->getAvatar() ?? '',
|
||||
]);
|
||||
|
||||
return $oauthConnection->user;
|
||||
}
|
||||
|
||||
// If the username already exists in the database, append a random string to it to ensure uniqueness.
|
||||
$username = $providerUser->getName() ?? $providerUser->getNickname();
|
||||
$random = '';
|
||||
while (User::whereName($username.$random)->exists()) {
|
||||
$random = '-'.Str::random(5);
|
||||
}
|
||||
$username .= $random;
|
||||
|
||||
// The user has not connected their account with this OAuth provider before, so a new connection needs to be
|
||||
// established. Check if the user has an account with the same email address that's passed in from the provider.
|
||||
// If one exists, connect that account. Otherwise, create a new one.
|
||||
|
||||
return DB::transaction(function () use ($providerUser, $provider, $username) {
|
||||
|
||||
$user = User::firstOrCreate(['email' => $providerUser->getEmail()], [
|
||||
'name' => $username,
|
||||
'password' => null,
|
||||
]);
|
||||
|
||||
$connection = $user->oAuthConnections()->create([
|
||||
'provider' => $provider,
|
||||
'provider_id' => $providerUser->getId(),
|
||||
'token' => $providerUser->token ?? '',
|
||||
'refresh_token' => $providerUser->refreshToken ?? '',
|
||||
'nickname' => $providerUser->getNickname() ?? '',
|
||||
'name' => $providerUser->getName() ?? '',
|
||||
'email' => $providerUser->getEmail() ?? '',
|
||||
'avatar' => $providerUser->getAvatar() ?? '',
|
||||
]);
|
||||
|
||||
$this->updateAvatar($user, $connection->avatar);
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
private function updateAvatar(User $user, string $avatarUrl): void
|
||||
{
|
||||
// Determine the disk to use based on the environment.
|
||||
$disk = match (config('app.env')) {
|
||||
'production' => 'r2', // Cloudflare R2 Storage
|
||||
default => 'public', // Local
|
||||
};
|
||||
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($curl, CURLOPT_URL, $avatarUrl);
|
||||
$image = curl_exec($curl);
|
||||
|
||||
if ($image === false) {
|
||||
Log::error('There was an error attempting to download the image. cURL error: '.curl_error($curl));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate a random path for the image and ensure that it doesn't already exist.
|
||||
do {
|
||||
$relativePath = User::profilePhotoStoragePath().'/'.Str::random(40).'.webp';
|
||||
} while (Storage::disk($disk)->exists($relativePath));
|
||||
|
||||
// Store the image on the disk.
|
||||
Storage::disk($disk)->put($relativePath, $image);
|
||||
|
||||
// Update the user's profile photo path.
|
||||
$user->forceFill([
|
||||
'profile_photo_path' => $relativePath,
|
||||
])->save();
|
||||
}
|
||||
}
|
@ -5,13 +5,28 @@ namespace App\Http\Controllers;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public function show(Request $request, User $user, string $username)
|
||||
public function show(Request $request, int $userId, string $username): View
|
||||
{
|
||||
$user = User::whereId($userId)
|
||||
->with(['following', 'followers'])
|
||||
->firstOrFail();
|
||||
|
||||
$mods = $user->mods()
|
||||
->with([
|
||||
'users',
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion',
|
||||
])
|
||||
->orderByDesc('created_at')
|
||||
->paginate(10)
|
||||
->fragment('mods');
|
||||
|
||||
if ($user->slug() !== $username) {
|
||||
abort(404);
|
||||
}
|
||||
@ -20,6 +35,6 @@ class UserController extends Controller
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('user.show', compact('user'));
|
||||
return view('user.show', compact('user', 'mods'));
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace App\Http\Filters;
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@ -15,14 +14,17 @@ class ModFilter
|
||||
protected Builder $builder;
|
||||
|
||||
/**
|
||||
* The filter that should be applied to the query.
|
||||
* The filters to apply.
|
||||
*/
|
||||
protected array $filters;
|
||||
|
||||
/**
|
||||
* Create a new ModFilter instance.
|
||||
*/
|
||||
public function __construct(array $filters)
|
||||
{
|
||||
$this->builder = $this->baseQuery();
|
||||
$this->filters = $filters;
|
||||
$this->builder = $this->baseQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -30,21 +32,29 @@ class ModFilter
|
||||
*/
|
||||
private function baseQuery(): Builder
|
||||
{
|
||||
return Mod::select([
|
||||
'mods.id',
|
||||
'mods.name',
|
||||
'mods.slug',
|
||||
'mods.teaser',
|
||||
'mods.thumbnail',
|
||||
'mods.featured',
|
||||
'mods.downloads',
|
||||
'mods.created_at',
|
||||
])->with([
|
||||
'users:id,name',
|
||||
'latestVersion' => function ($query) {
|
||||
$query->with('latestSptVersion:id,version,color_class');
|
||||
},
|
||||
]);
|
||||
return Mod::query()
|
||||
->select('mods.*')
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('mod_versions')
|
||||
->join('mod_version_spt_version', 'mod_versions.id', '=', 'mod_version_spt_version.mod_version_id')
|
||||
->join('spt_versions', 'mod_version_spt_version.spt_version_id', '=', 'spt_versions.id')
|
||||
->whereColumn('mod_versions.mod_id', 'mods.id')
|
||||
->where('spt_versions.version', '!=', '0.0.0');
|
||||
})
|
||||
->with([
|
||||
'users:id,name',
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the results by the given search term.
|
||||
*/
|
||||
private function query(string $term): Builder
|
||||
{
|
||||
return $this->builder->whereLike('mods.name', "%{$term}%");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,8 +68,6 @@ class ModFilter
|
||||
}
|
||||
}
|
||||
|
||||
//dd($this->builder->toRawSql());
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
@ -68,34 +76,11 @@ class ModFilter
|
||||
*/
|
||||
private function order(string $type): Builder
|
||||
{
|
||||
// We order the "recently updated" mods by the ModVersion's updated_at value.
|
||||
if ($type === 'updated') {
|
||||
return $this->builder
|
||||
->joinSub(
|
||||
ModVersion::select('mod_id', DB::raw('MAX(updated_at) as latest_updated_at'))->groupBy('mod_id'),
|
||||
'latest_versions',
|
||||
'mods.id',
|
||||
'=',
|
||||
'latest_versions.mod_id'
|
||||
)
|
||||
->orderByDesc('latest_versions.latest_updated_at');
|
||||
}
|
||||
|
||||
// By default, we simply order by the column on the mods table/query.
|
||||
$column = match ($type) {
|
||||
'downloaded' => 'downloads',
|
||||
default => 'created_at',
|
||||
return match ($type) {
|
||||
'updated' => $this->builder->orderByDesc('mods.updated_at'),
|
||||
'downloaded' => $this->builder->orderByDesc('mods.downloads'),
|
||||
default => $this->builder->orderByDesc('mods.created_at'),
|
||||
};
|
||||
|
||||
return $this->builder->orderByDesc($column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the results by the given search term.
|
||||
*/
|
||||
private function query(string $term): Builder
|
||||
{
|
||||
return $this->builder->whereLike('name', "%$term%");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,8 +89,8 @@ class ModFilter
|
||||
private function featured(string $option): Builder
|
||||
{
|
||||
return match ($option) {
|
||||
'exclude' => $this->builder->where('featured', false),
|
||||
'only' => $this->builder->where('featured', true),
|
||||
'exclude' => $this->builder->where('mods.featured', false),
|
||||
'only' => $this->builder->where('mods.featured', true),
|
||||
default => $this->builder,
|
||||
};
|
||||
}
|
||||
@ -115,28 +100,14 @@ class ModFilter
|
||||
*/
|
||||
private function sptVersions(array $versions): Builder
|
||||
{
|
||||
// Parse the versions into major, minor, and patch arrays
|
||||
$parsedVersions = array_map(fn ($version) => [
|
||||
'major' => (int) explode('.', $version)[0],
|
||||
'minor' => (int) (explode('.', $version)[1] ?? 0),
|
||||
'patch' => (int) (explode('.', $version)[2] ?? 0),
|
||||
], $versions);
|
||||
|
||||
[$majorVersions, $minorVersions, $patchVersions] = array_map('array_unique', [
|
||||
array_column($parsedVersions, 'major'),
|
||||
array_column($parsedVersions, 'minor'),
|
||||
array_column($parsedVersions, 'patch'),
|
||||
]);
|
||||
|
||||
return $this->builder
|
||||
->join('mod_versions as mv', 'mods.id', '=', 'mv.mod_id')
|
||||
->join('mod_version_spt_version as mvsv', 'mv.id', '=', 'mvsv.mod_version_id')
|
||||
->join('spt_versions as sv', 'mvsv.spt_version_id', '=', 'sv.id')
|
||||
->whereIn('sv.version_major', $majorVersions)
|
||||
->whereIn('sv.version_minor', $minorVersions)
|
||||
->whereIn('sv.version_patch', $patchVersions)
|
||||
->where('sv.version', '!=', '0.0.0')
|
||||
->groupBy('mods.id')
|
||||
->distinct();
|
||||
return $this->builder->whereExists(function ($query) use ($versions) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('mod_versions')
|
||||
->join('mod_version_spt_version', 'mod_versions.id', '=', 'mod_version_spt_version.mod_version_id')
|
||||
->join('spt_versions', 'mod_version_spt_version.spt_version_id', '=', 'spt_versions.id')
|
||||
->whereColumn('mod_versions.mod_id', 'mods.id')
|
||||
->whereIn('spt_versions.version', $versions)
|
||||
->where('spt_versions.version', '!=', '0.0.0');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,12 @@
|
||||
namespace App\Http\Filters\V1;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ModFilter extends QueryFilter
|
||||
{
|
||||
/**
|
||||
* The sortable fields.
|
||||
*/
|
||||
protected array $sortable = [
|
||||
'name',
|
||||
'slug',
|
||||
@ -20,124 +22,99 @@ class ModFilter extends QueryFilter
|
||||
'published_at',
|
||||
];
|
||||
|
||||
// TODO: Many of these are repeated across UserFilter and ModFilter. Consider refactoring into a shared trait.
|
||||
// Also, consider using common filter types and making the field names dynamic.
|
||||
|
||||
/**
|
||||
* Filter by ID.
|
||||
*/
|
||||
public function id(string $value): Builder
|
||||
{
|
||||
$ids = array_map('trim', explode(',', $value));
|
||||
|
||||
return $this->builder->whereIn('id', $ids);
|
||||
return $this->filterWhereIn('id', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by hub ID.
|
||||
*/
|
||||
public function hub_id(string $value): Builder
|
||||
{
|
||||
$ids = array_map('trim', explode(',', $value));
|
||||
|
||||
return $this->builder->whereIn('hub_id', $ids);
|
||||
return $this->filterWhereIn('hub_id', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by name.
|
||||
*/
|
||||
public function name(string $value): Builder
|
||||
{
|
||||
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||
$like = Str::replace('*', '%', $value);
|
||||
|
||||
return $this->builder->where('name', 'like', $like);
|
||||
return $this->filterByWildcardLike('name', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by slug.
|
||||
*/
|
||||
public function slug(string $value): Builder
|
||||
{
|
||||
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||
$like = Str::replace('*', '%', $value);
|
||||
|
||||
return $this->builder->where('slug', 'like', $like);
|
||||
return $this->filterByWildcardLike('slug', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by teaser.
|
||||
*/
|
||||
public function teaser(string $value): Builder
|
||||
{
|
||||
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||
$like = Str::replace('*', '%', $value);
|
||||
|
||||
return $this->builder->where('teaser', 'like', $like);
|
||||
return $this->filterByWildcardLike('teaser', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by source code link.
|
||||
*/
|
||||
public function source_code_link(string $value): Builder
|
||||
{
|
||||
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||
$like = Str::replace('*', '%', $value);
|
||||
|
||||
return $this->builder->where('source_code_link', 'like', $like);
|
||||
return $this->filterByWildcardLike('source_code_link', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by created at date.
|
||||
*/
|
||||
public function created_at(string $value): Builder
|
||||
{
|
||||
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||
$dates = array_map('trim', explode(',', $value));
|
||||
if (count($dates) > 1) {
|
||||
return $this->builder->whereBetween('created_at', $dates);
|
||||
}
|
||||
|
||||
return $this->builder->whereDate('created_at', $value);
|
||||
return $this->filterByDate('created_at', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by updated at date.
|
||||
*/
|
||||
public function updated_at(string $value): Builder
|
||||
{
|
||||
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||
$dates = array_map('trim', explode(',', $value));
|
||||
if (count($dates) > 1) {
|
||||
return $this->builder->whereBetween('updated_at', $dates);
|
||||
}
|
||||
|
||||
return $this->builder->whereDate('updated_at', $value);
|
||||
return $this->filterByDate('updated_at', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by published at date.
|
||||
*/
|
||||
public function published_at(string $value): Builder
|
||||
{
|
||||
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||
$dates = array_map('trim', explode(',', $value));
|
||||
if (count($dates) > 1) {
|
||||
return $this->builder->whereBetween('published_at', $dates);
|
||||
}
|
||||
|
||||
return $this->builder->whereDate('published_at', $value);
|
||||
return $this->filterByDate('published_at', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by featured.
|
||||
*/
|
||||
public function featured(string $value): Builder
|
||||
{
|
||||
// We need to convert the string user input to a boolean, or null if it's not a valid "truthy/falsy" value.
|
||||
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
|
||||
// This column is not nullable.
|
||||
if ($value === null) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
return $this->builder->where('featured', $value);
|
||||
return $this->filterByBoolean('featured', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by contains ads.
|
||||
*/
|
||||
public function contains_ads(string $value): Builder
|
||||
{
|
||||
// We need to convert the string user input to a boolean, or null if it's not a valid "truthy/falsy" value.
|
||||
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
|
||||
// This column is not nullable.
|
||||
if ($value === null) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
return $this->builder->where('contains_ads', $value);
|
||||
return $this->filterByBoolean('contains_ads', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by contains AI content.
|
||||
*/
|
||||
public function contains_ai_content(string $value): Builder
|
||||
{
|
||||
// We need to convert the string user input to a boolean, or null if it's not a valid "truthy/falsy" value.
|
||||
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
|
||||
// This column is not nullable.
|
||||
if ($value === null) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
return $this->builder->where('contains_ai_content', $value);
|
||||
return $this->filterByBoolean('contains_ai_content', $value);
|
||||
}
|
||||
}
|
||||
|
@ -2,23 +2,57 @@
|
||||
|
||||
namespace App\Http\Filters\V1;
|
||||
|
||||
use App\Traits\V1\FilterMethods;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class QueryFilter
|
||||
{
|
||||
/**
|
||||
* Include general filter methods.
|
||||
*/
|
||||
use FilterMethods;
|
||||
|
||||
/**
|
||||
* The query builder instance.
|
||||
*/
|
||||
protected Builder $builder;
|
||||
|
||||
/**
|
||||
* The request instance.
|
||||
*/
|
||||
protected Request $request;
|
||||
|
||||
/**
|
||||
* The sortable fields.
|
||||
*/
|
||||
protected array $sortable = [];
|
||||
|
||||
/**
|
||||
* Create a new QueryFilter instance.
|
||||
*/
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over each of the filter options and call the appropriate method if it exists.
|
||||
*/
|
||||
public function filter(array $filters): Builder
|
||||
{
|
||||
foreach ($filters as $attribute => $value) {
|
||||
if (method_exists($this, $attribute)) {
|
||||
$this->$attribute($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over all request data and call the appropriate method if it exists.
|
||||
*/
|
||||
public function apply(Builder $builder): Builder
|
||||
{
|
||||
$this->builder = $builder;
|
||||
@ -31,31 +65,4 @@ abstract class QueryFilter
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
protected function filter(array $filters): Builder
|
||||
{
|
||||
foreach ($filters as $attribute => $value) {
|
||||
if (method_exists($this, $attribute)) {
|
||||
$this->$attribute($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
protected function sort(string $values): Builder
|
||||
{
|
||||
$sortables = array_map('trim', explode(',', $values));
|
||||
|
||||
foreach ($sortables as $sortable) {
|
||||
$direction = Str::startsWith($sortable, '-') ? 'desc' : 'asc';
|
||||
$column = Str::of($sortable)->remove('-')->value();
|
||||
|
||||
if (in_array($column, $this->sortable)) {
|
||||
$this->builder->orderBy($column, $direction);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
}
|
||||
|
@ -3,53 +3,47 @@
|
||||
namespace App\Http\Filters\V1;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UserFilter extends QueryFilter
|
||||
{
|
||||
/**
|
||||
* The sortable fields.
|
||||
*/
|
||||
protected array $sortable = [
|
||||
'name',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
// TODO: Many of these are repeated across UserFilter and ModFilter. Consider refactoring into a shared trait.
|
||||
// Also, consider using common filter types and making the field names dynamic.
|
||||
|
||||
/**
|
||||
* Filter by ID.
|
||||
*/
|
||||
public function id(string $value): Builder
|
||||
{
|
||||
$ids = array_map('trim', explode(',', $value));
|
||||
|
||||
return $this->builder->whereIn('id', $ids);
|
||||
return $this->filterWhereIn('id', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by name.
|
||||
*/
|
||||
public function name(string $value): Builder
|
||||
{
|
||||
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||
$like = Str::replace('*', '%', $value);
|
||||
|
||||
return $this->builder->where('name', 'like', $like);
|
||||
return $this->filterByWildcardLike('name', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by created at date.
|
||||
*/
|
||||
public function created_at(string $value): Builder
|
||||
{
|
||||
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||
$dates = array_map('trim', explode(',', $value));
|
||||
if (count($dates) > 1) {
|
||||
return $this->builder->whereBetween('created_at', $dates);
|
||||
}
|
||||
|
||||
return $this->builder->whereDate('created_at', $value);
|
||||
return $this->filterByDate('created_at', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by updated at date.
|
||||
*/
|
||||
public function updated_at(string $value): Builder
|
||||
{
|
||||
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||
$dates = array_map('trim', explode(',', $value));
|
||||
if (count($dates) > 1) {
|
||||
return $this->builder->whereBetween('updated_at', $dates);
|
||||
}
|
||||
|
||||
return $this->builder->whereDate('updated_at', $value);
|
||||
return $this->filterByDate('updated_at', $value);
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,6 @@ class StoreModRequest extends FormRequest
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,6 @@ class StoreUserRequest extends FormRequest
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,6 @@ class UpdateModRequest extends FormRequest
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,6 @@ class UpdateUserRequest extends FormRequest
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,9 @@ use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ModRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
@ -18,6 +21,9 @@ class ModRequest extends FormRequest
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
|
@ -9,6 +9,9 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
||||
/** @mixin License */
|
||||
class LicenseResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
|
@ -15,6 +15,8 @@ class ModResource extends JsonResource
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$this->load(['users', 'versions', 'license']);
|
||||
|
||||
return [
|
||||
'type' => 'mod',
|
||||
'id' => $this->id,
|
||||
@ -51,11 +53,8 @@ class ModResource extends JsonResource
|
||||
'type' => 'version',
|
||||
'id' => $version->id,
|
||||
],
|
||||
|
||||
// TODO: The download link to the version can be placed here, but I'd like to track the number of
|
||||
// downloads that are made, so we'll need a new route/feature for that. #35
|
||||
'links' => [
|
||||
'self' => $version->link,
|
||||
'self' => $version->downloadUrl(absolute: true),
|
||||
],
|
||||
|
||||
])->toArray(),
|
||||
|
@ -28,11 +28,7 @@ class ModVersionResource extends JsonResource
|
||||
// $this->description
|
||||
//),
|
||||
|
||||
// TODO: The download link to the version can be placed here, but I'd like to track the number of
|
||||
// downloads that are made, so we'll need a new route/feature for that. #35
|
||||
'link' => $this->link,
|
||||
|
||||
'spt_version_id' => $this->spt_version_id,
|
||||
'link' => $this->downloadUrl(absolute: true),
|
||||
'virus_total_link' => $this->virus_total_link,
|
||||
'downloads' => $this->downloads,
|
||||
'created_at' => $this->created_at,
|
||||
@ -44,7 +40,6 @@ class ModVersionResource extends JsonResource
|
||||
[
|
||||
'data' => [
|
||||
'type' => 'spt_version',
|
||||
'id' => $this->spt_version_id,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
@ -10,8 +10,13 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
||||
/** @mixin User */
|
||||
class UserResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$this->load('role');
|
||||
|
||||
return [
|
||||
'type' => 'user',
|
||||
'id' => $this->id,
|
||||
@ -31,7 +36,7 @@ class UserResource extends JsonResource
|
||||
],
|
||||
'includes' => $this->when(
|
||||
ApiController::shouldInclude('user_role'),
|
||||
new UserRoleResource($this->role)
|
||||
new UserRoleResource($this->role),
|
||||
),
|
||||
'links' => [
|
||||
'self' => $this->profileUrl(),
|
||||
|
@ -9,6 +9,9 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
||||
/** @mixin UserRole */
|
||||
class UserRoleResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
|
@ -2,12 +2,16 @@
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\License;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/** @mixin \App\Models\License */
|
||||
/** @mixin License */
|
||||
class LicenseResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
|
@ -2,12 +2,16 @@
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\Mod;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/** @mixin \App\Models\Mod */
|
||||
/** @mixin Mod */
|
||||
class ModResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
|
@ -2,12 +2,16 @@
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\ModVersion;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/** @mixin \App\Models\ModVersion */
|
||||
/** @mixin ModVersion */
|
||||
class ModVersionResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
@ -19,10 +23,7 @@ class ModVersionResource extends JsonResource
|
||||
'description' => $this->description,
|
||||
'virus_total_link' => $this->virus_total_link,
|
||||
'downloads' => $this->downloads,
|
||||
|
||||
'mod_id' => $this->mod_id,
|
||||
'spt_version_id' => $this->spt_version_id,
|
||||
|
||||
'mod' => new ModResource($this->whenLoaded('mod')),
|
||||
'sptVersion' => new SptVersionResource($this->whenLoaded('sptVersion')),
|
||||
];
|
||||
|
@ -2,12 +2,16 @@
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/** @mixin \App\Models\SptVersion */
|
||||
/** @mixin SptVersion */
|
||||
class SptVersionResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
|
23
app/Jobs/Import/DataTransferObjects/HubUser.php
Normal file
23
app/Jobs/Import/DataTransferObjects/HubUser.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Import\DataTransferObjects;
|
||||
|
||||
class HubUser
|
||||
{
|
||||
public function __construct(
|
||||
public int $userID,
|
||||
public string $username,
|
||||
public string $email,
|
||||
public string $password,
|
||||
public string $registrationDate,
|
||||
public ?bool $banned,
|
||||
public ?string $banReason,
|
||||
public ?string $banExpires,
|
||||
public ?string $coverPhotoHash,
|
||||
public ?string $coverPhotoExtension,
|
||||
public ?int $rankID,
|
||||
public ?string $rankTitle,
|
||||
) {
|
||||
//
|
||||
}
|
||||
}
|
@ -1,13 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
namespace App\Jobs\Import;
|
||||
|
||||
use App\Exceptions\InvalidVersionNumberException;
|
||||
use App\Jobs\Import\DataTransferObjects\HubUser;
|
||||
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 App\Support\Version;
|
||||
use Carbon\Carbon;
|
||||
use CurlHandle;
|
||||
use Exception;
|
||||
@ -27,6 +30,7 @@ use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
use Stevebauman\Purify\Facades\Purify;
|
||||
use Throwable;
|
||||
|
||||
class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
@ -37,6 +41,7 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
// 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->bringUserAvatarLocal();
|
||||
$this->bringUserOptionsLocal();
|
||||
$this->bringFileAuthorsLocal();
|
||||
$this->bringFileOptionsLocal();
|
||||
$this->bringFileContentLocal();
|
||||
@ -46,6 +51,7 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
|
||||
// Begin to import the data into the permanent local database tables.
|
||||
$this->importUsers();
|
||||
$this->importUserFollows();
|
||||
$this->importLicenses();
|
||||
$this->importSptVersions();
|
||||
$this->importMods();
|
||||
@ -97,6 +103,35 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bring the user options table from the Hub database to the local database temporary table.
|
||||
*/
|
||||
private function bringUserOptionsLocal(): void
|
||||
{
|
||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_user_options_values');
|
||||
DB::statement('CREATE TEMPORARY TABLE temp_user_options_values (
|
||||
userID INT,
|
||||
about LONGTEXT
|
||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('wcf1_user_option_value')
|
||||
->orderBy('userID')
|
||||
->chunk(200, function ($options) {
|
||||
$insertData = [];
|
||||
foreach ($options as $option) {
|
||||
$insertData[] = [
|
||||
'userID' => (int) $option->userID,
|
||||
'about' => $option->userOption1,
|
||||
];
|
||||
}
|
||||
|
||||
if ($insertData) {
|
||||
DB::table('temp_user_options_values')->insert($insertData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bring the file authors from the Hub database to the local database temporary table.
|
||||
*/
|
||||
@ -309,14 +344,29 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
$userData = $bannedUsers = $userRanks = [];
|
||||
|
||||
foreach ($users as $user) {
|
||||
$userData[] = $this->collectUserData($curl, $user);
|
||||
$hubUser = new HubUser(
|
||||
$user->userID,
|
||||
$user->username,
|
||||
$user->email,
|
||||
$user->password,
|
||||
$user->registrationDate,
|
||||
$user->banned,
|
||||
$user->banReason,
|
||||
$user->banExpires,
|
||||
$user->coverPhotoHash,
|
||||
$user->coverPhotoExtension,
|
||||
$user->rankID,
|
||||
$user->rankTitle
|
||||
);
|
||||
|
||||
$bannedUserData = $this->collectBannedUserData($user);
|
||||
$userData[] = $this->collectUserData($curl, $hubUser);
|
||||
|
||||
$bannedUserData = $this->collectBannedUserData($hubUser);
|
||||
if ($bannedUserData) {
|
||||
$bannedUsers[] = $bannedUserData;
|
||||
}
|
||||
|
||||
$userRankData = $this->collectUserRankData($user);
|
||||
$userRankData = $this->collectUserRankData($hubUser);
|
||||
if ($userRankData) {
|
||||
$userRanks[] = $userRankData;
|
||||
}
|
||||
@ -331,16 +381,20 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
curl_close($curl);
|
||||
}
|
||||
|
||||
protected function collectUserData(CurlHandle $curl, object $user): array
|
||||
/**
|
||||
* Build an array of user data ready to be inserted into the local database.
|
||||
*/
|
||||
protected function collectUserData(CurlHandle $curl, HubUser $hubUser): array
|
||||
{
|
||||
return [
|
||||
'hub_id' => (int) $user->userID,
|
||||
'name' => $user->username,
|
||||
'email' => Str::lower($user->email),
|
||||
'password' => $this->cleanPasswordHash($user->password),
|
||||
'profile_photo_path' => $this->fetchUserAvatar($curl, $user),
|
||||
'cover_photo_path' => $this->fetchUserCoverPhoto($curl, $user),
|
||||
'created_at' => $this->cleanRegistrationDate($user->registrationDate),
|
||||
'hub_id' => (int) $hubUser->userID,
|
||||
'name' => $hubUser->username,
|
||||
'email' => Str::lower($hubUser->email),
|
||||
'password' => $this->cleanPasswordHash($hubUser->password),
|
||||
'about' => $this->fetchUserAbout($hubUser->userID),
|
||||
'profile_photo_path' => $this->fetchUserAvatar($curl, $hubUser),
|
||||
'cover_photo_path' => $this->fetchUserCoverPhoto($curl, $hubUser),
|
||||
'created_at' => $this->cleanRegistrationDate($hubUser->registrationDate),
|
||||
'updated_at' => now('UTC')->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
@ -358,13 +412,39 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
return str_starts_with($clean, '$2') ? $clean : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the user about text from the temporary table.
|
||||
*/
|
||||
private function fetchUserAbout(int $userID): string
|
||||
{
|
||||
$about = DB::table('temp_user_options_values')
|
||||
->where('userID', $userID)
|
||||
->limit(1)
|
||||
->value('about');
|
||||
|
||||
return $this->cleanHubContent($about ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 user avatar from the Hub and store it anew.
|
||||
*/
|
||||
protected function fetchUserAvatar(CurlHandle $curl, object $user): string
|
||||
protected function fetchUserAvatar(CurlHandle $curl, HubUser $hubUser): string
|
||||
{
|
||||
// Fetch the user's avatar data from the temporary table.
|
||||
$avatar = DB::table('temp_user_avatar')->where('userID', $user->userID)->first();
|
||||
$avatar = DB::table('temp_user_avatar')->where('userID', $hubUser->userID)->first();
|
||||
|
||||
if (! $avatar) {
|
||||
return '';
|
||||
@ -373,7 +453,7 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
$hashShort = substr($avatar->fileHash, 0, 2);
|
||||
$fileName = $avatar->fileHash.'.'.$avatar->avatarExtension;
|
||||
$hubUrl = 'https://hub.sp-tarkov.com/images/avatars/'.$hashShort.'/'.$avatar->avatarID.'-'.$fileName;
|
||||
$relativePath = 'user-avatars/'.$fileName;
|
||||
$relativePath = User::profilePhotoStoragePath().'/'.$fileName;
|
||||
|
||||
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||
}
|
||||
@ -413,15 +493,15 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
/**
|
||||
* Fetch the user avatar from the Hub and store it anew.
|
||||
*/
|
||||
protected function fetchUserCoverPhoto(CurlHandle $curl, object $user): string
|
||||
protected function fetchUserCoverPhoto(CurlHandle $curl, HubUser $hubUser): string
|
||||
{
|
||||
if (empty($user->coverPhotoHash) || empty($user->coverPhotoExtension)) {
|
||||
if (empty($hubUser->coverPhotoHash) || empty($hubUser->coverPhotoExtension)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hashShort = substr($user->coverPhotoHash, 0, 2);
|
||||
$fileName = $user->coverPhotoHash.'.'.$user->coverPhotoExtension;
|
||||
$hubUrl = 'https://hub.sp-tarkov.com/images/coverPhotos/'.$hashShort.'/'.$user->userID.'-'.$fileName;
|
||||
$hashShort = substr($hubUser->coverPhotoHash, 0, 2);
|
||||
$fileName = $hubUser->coverPhotoHash.'.'.$hubUser->coverPhotoExtension;
|
||||
$hubUrl = 'https://hub.sp-tarkov.com/images/coverPhotos/'.$hashShort.'/'.$hubUser->userID.'-'.$fileName;
|
||||
$relativePath = 'user-covers/'.$fileName;
|
||||
|
||||
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||
@ -445,13 +525,13 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
/**
|
||||
* Build an array of banned user data ready to be inserted into the local database.
|
||||
*/
|
||||
protected function collectBannedUserData($user): ?array
|
||||
protected function collectBannedUserData(HubUser $hubUser): ?array
|
||||
{
|
||||
if ($user->banned) {
|
||||
if ($hubUser->banned) {
|
||||
return [
|
||||
'hub_id' => (int) $user->userID,
|
||||
'comment' => $user->banReason ?? '',
|
||||
'expired_at' => $this->cleanUnbannedAtDate($user->banExpires),
|
||||
'hub_id' => (int) $hubUser->userID,
|
||||
'comment' => $hubUser->banReason ?? '',
|
||||
'expired_at' => $this->cleanUnbannedAtDate($hubUser->banExpires),
|
||||
];
|
||||
}
|
||||
|
||||
@ -498,12 +578,15 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
protected function collectUserRankData($user): ?array
|
||||
/**
|
||||
* Build an array of user rank data ready to be inserted into the local database.
|
||||
*/
|
||||
protected function collectUserRankData(HubUser $hubUser): ?array
|
||||
{
|
||||
if ($user->rankID && $user->rankTitle) {
|
||||
if ($hubUser->rankID && $hubUser->rankTitle) {
|
||||
return [
|
||||
'hub_id' => (int) $user->userID,
|
||||
'title' => $user->rankTitle,
|
||||
'hub_id' => (int) $hubUser->userID,
|
||||
'title' => $hubUser->rankTitle,
|
||||
];
|
||||
}
|
||||
|
||||
@ -513,7 +596,7 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
/**
|
||||
* Insert or update the users in the local database.
|
||||
*/
|
||||
protected function upsertUsers($usersData): void
|
||||
protected function upsertUsers(array $usersData): void
|
||||
{
|
||||
if (! empty($usersData)) {
|
||||
DB::table('users')->upsert($usersData, ['hub_id'], [
|
||||
@ -529,7 +612,7 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
/**
|
||||
* Fetch the hub-banned users from the local database and ban them locally.
|
||||
*/
|
||||
protected function handleBannedUsers($bannedUsers): void
|
||||
protected function handleBannedUsers(array $bannedUsers): void
|
||||
{
|
||||
foreach ($bannedUsers as $bannedUser) {
|
||||
$user = User::whereHubId($bannedUser['hub_id'])->first();
|
||||
@ -543,7 +626,7 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
/**
|
||||
* Fetch or create the user ranks in the local database and assign them to the users.
|
||||
*/
|
||||
protected function handleUserRoles($userRanks): void
|
||||
protected function handleUserRoles(array $userRanks): void
|
||||
{
|
||||
foreach ($userRanks as $userRank) {
|
||||
$roleName = Str::ucfirst(Str::afterLast($userRank['title'], '.'));
|
||||
@ -587,6 +670,37 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
];
|
||||
}
|
||||
|
||||
protected function importUserFollows(): void
|
||||
{
|
||||
$followsGroupedByFollower = [];
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('wcf1_user_follow')
|
||||
->select(['followID', 'userID', 'followUserID', 'time'])
|
||||
->chunkById(100, function (Collection $follows) use (&$followsGroupedByFollower) {
|
||||
foreach ($follows as $follow) {
|
||||
$followerId = User::whereHubId($follow->userID)->value('id');
|
||||
$followingId = User::whereHubId($follow->followUserID)->value('id');
|
||||
|
||||
if (! $followerId || ! $followingId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$followsGroupedByFollower[$followerId][$followingId] = [
|
||||
'created_at' => Carbon::parse($follow->time, 'UTC'),
|
||||
'updated_at' => Carbon::parse($follow->time, 'UTC'),
|
||||
];
|
||||
}
|
||||
}, 'followID');
|
||||
|
||||
foreach ($followsGroupedByFollower as $followerId => $followings) {
|
||||
$user = User::find($followerId);
|
||||
if ($user) {
|
||||
$user->following()->sync($followings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the licenses from the Hub database to the local database.
|
||||
*/
|
||||
@ -861,19 +975,6 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
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.
|
||||
*/
|
||||
@ -934,10 +1035,20 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
$sptVersionTemp = DB::table('temp_spt_version_tags')->where('hub_id', $versionLabel->labelID)->value('version');
|
||||
$sptVersionConstraint = $this->extractSemanticVersion($sptVersionTemp, appendPatch: true) ?? '0.0.0';
|
||||
|
||||
try {
|
||||
$modVersion = new Version($version->versionNumber);
|
||||
} catch (InvalidVersionNumberException $e) {
|
||||
$modVersion = new Version('0.0.0');
|
||||
}
|
||||
|
||||
$insertData[] = [
|
||||
'hub_id' => (int) $version->versionID,
|
||||
'mod_id' => $modId,
|
||||
'version' => $this->extractSemanticVersion($version->versionNumber) ?? '0.0.0',
|
||||
'version' => $modVersion,
|
||||
'version_major' => $modVersion->getMajor(),
|
||||
'version_minor' => $modVersion->getMinor(),
|
||||
'version_patch' => $modVersion->getPatch(),
|
||||
'version_pre_release' => $modVersion->getPreRelease(),
|
||||
'description' => $this->cleanHubContent($versionContent->description ?? ''),
|
||||
'link' => $version->downloadURL,
|
||||
'spt_version_constraint' => $sptVersionConstraint,
|
||||
@ -984,10 +1095,11 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
/**
|
||||
* The job failed to process.
|
||||
*/
|
||||
public function failed(Exception $exception): void
|
||||
public function failed(Throwable $exception): void
|
||||
{
|
||||
// Explicitly drop the temporary tables.
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_user_avatar');
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_user_options_values');
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_author');
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
@ -4,8 +4,10 @@ namespace App\Livewire;
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
class GlobalSearch extends Component
|
||||
@ -16,20 +18,26 @@ class GlobalSearch extends Component
|
||||
public string $query = '';
|
||||
|
||||
/**
|
||||
* Whether to show the search result dropdown.
|
||||
* The search results.
|
||||
*/
|
||||
public bool $showDropdown = false;
|
||||
#[Locked]
|
||||
public array $result = [];
|
||||
|
||||
/**
|
||||
* Whether to show the "no results found" message.
|
||||
* The total number of search results.
|
||||
*/
|
||||
public bool $noResults = false;
|
||||
#[Locked]
|
||||
public int $count = 0;
|
||||
|
||||
/**
|
||||
* Render the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.global-search', [
|
||||
'results' => $this->executeSearch($this->query),
|
||||
]);
|
||||
$this->result = $this->executeSearch($this->query);
|
||||
$this->count = $this->countTotalResults($this->result);
|
||||
|
||||
return view('livewire.global-search');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -38,39 +46,40 @@ class GlobalSearch extends Component
|
||||
protected function executeSearch(string $query): array
|
||||
{
|
||||
$query = Str::trim($query);
|
||||
$results = ['data' => [], 'total' => 0];
|
||||
|
||||
if (Str::length($query)) {
|
||||
$results['data'] = [
|
||||
'user' => collect(User::search($query)->raw()['hits']),
|
||||
'mod' => collect(Mod::search($query)->raw()['hits']),
|
||||
if (Str::length($query) > 0) {
|
||||
return [
|
||||
'user' => $this->fetchUserResults($query),
|
||||
'mod' => $this->fetchModResults($query),
|
||||
];
|
||||
$results['total'] = $this->countTotalResults($results['data']);
|
||||
}
|
||||
|
||||
$this->showDropdown = Str::length($query) > 0;
|
||||
$this->noResults = $results['total'] === 0 && $this->showDropdown;
|
||||
return [];
|
||||
}
|
||||
|
||||
return $results;
|
||||
/**
|
||||
* Fetch the user search results.
|
||||
*/
|
||||
protected function fetchUserResults(string $query): Collection
|
||||
{
|
||||
return collect(User::search($query)->raw()['hits']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the mod search results.
|
||||
*/
|
||||
protected function fetchModResults(string $query): Collection
|
||||
{
|
||||
return collect(Mod::search($query)->raw()['hits']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the total number of results across all models.
|
||||
*/
|
||||
protected function countTotalResults($results): int
|
||||
protected function countTotalResults(array $results): int
|
||||
{
|
||||
return collect($results)->reduce(function ($carry, $result) {
|
||||
return collect($results)->reduce(function (int $carry, Collection $result) {
|
||||
return $carry + $result->count();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the search query and hide the dropdown.
|
||||
*/
|
||||
public function clearSearch(): void
|
||||
{
|
||||
$this->query = '';
|
||||
$this->showDropdown = false;
|
||||
$this->noResults = false;
|
||||
}
|
||||
}
|
||||
|
@ -4,62 +4,87 @@ namespace App\Livewire\Mod;
|
||||
|
||||
use App\Http\Filters\ModFilter;
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Session;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class Index extends Component
|
||||
class Listing extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
/**
|
||||
* The search query value.
|
||||
*/
|
||||
#[Url]
|
||||
#[Session]
|
||||
#[Url]
|
||||
public string $query = '';
|
||||
|
||||
/**
|
||||
* The sort order value.
|
||||
*/
|
||||
#[Url]
|
||||
#[Session]
|
||||
#[Url]
|
||||
public string $order = 'created';
|
||||
|
||||
/**
|
||||
* The number of results to show on a single page.
|
||||
*/
|
||||
#[Session]
|
||||
#[Url]
|
||||
public int $perPage = 12;
|
||||
|
||||
/**
|
||||
* The options that are available for the per page setting.
|
||||
*/
|
||||
#[Locked]
|
||||
public array $perPageOptions = [6, 12, 24, 50];
|
||||
|
||||
/**
|
||||
* The SPT versions filter value.
|
||||
*/
|
||||
#[Url]
|
||||
#[Session]
|
||||
#[Url]
|
||||
public array $sptVersions = [];
|
||||
|
||||
/**
|
||||
* The featured filter value.
|
||||
*/
|
||||
#[Url]
|
||||
#[Session]
|
||||
#[Url]
|
||||
public string $featured = 'include';
|
||||
|
||||
/**
|
||||
* The available SPT versions.
|
||||
*
|
||||
* @var Collection<int, SptVersion>
|
||||
*/
|
||||
public Collection $availableSptVersions;
|
||||
public Collection $activeSptVersions;
|
||||
|
||||
/**
|
||||
* The component mount method, run only once when the component is mounted.
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->availableSptVersions = $this->availableSptVersions ?? Cache::remember('available-spt-versions', 60 * 60, function () {
|
||||
$this->activeSptVersions = $this->activeSptVersions ?? Cache::remember('active-spt-versions', 60 * 60, function () {
|
||||
return SptVersion::getVersionsForLastThreeMinors();
|
||||
});
|
||||
|
||||
$this->sptVersions = $this->sptVersions ?? $this->getLatestMinorVersions()->pluck('version')->toArray();
|
||||
$this->sptVersions = $this->sptVersions ?? $this->getDefaultSptVersions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default values for the SPT Versions filter.
|
||||
*/
|
||||
protected function getDefaultSptVersions(): array
|
||||
{
|
||||
return $this->getLatestMinorVersions()->pluck('version')->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,7 +92,7 @@ class Index extends Component
|
||||
*/
|
||||
public function getLatestMinorVersions(): Collection
|
||||
{
|
||||
return $this->availableSptVersions->filter(function (SptVersion $sptVersion) {
|
||||
return $this->activeSptVersions->filter(function (SptVersion $sptVersion) {
|
||||
return $sptVersion->isLatestMinor();
|
||||
});
|
||||
}
|
||||
@ -77,6 +102,8 @@ class Index extends Component
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
$this->validatePerPage();
|
||||
|
||||
// Fetch the mods using the filters saved to the component properties.
|
||||
$filters = [
|
||||
'query' => $this->query,
|
||||
@ -84,14 +111,40 @@ class Index extends Component
|
||||
'order' => $this->order,
|
||||
'sptVersions' => $this->sptVersions,
|
||||
];
|
||||
$mods = (new ModFilter($filters))->apply()->paginate(16);
|
||||
|
||||
// Check if the current page is greater than the last page. Redirect if it is.
|
||||
$mods = (new ModFilter($filters))->apply()->paginate($this->perPage);
|
||||
|
||||
$this->redirectOutOfBoundsPage($mods);
|
||||
|
||||
return view('livewire.mod.listing', compact('mods'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the option selected is an option that is available by setting it to the closest available version.
|
||||
*/
|
||||
public function validatePerPage(): void
|
||||
{
|
||||
$this->perPage = collect($this->perPageOptions)->pipe(function ($data) {
|
||||
$closest = null;
|
||||
|
||||
foreach ($data as $item) {
|
||||
if ($closest === null || abs($this->perPage - $closest) > abs($item - $this->perPage)) {
|
||||
$closest = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $closest;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current page is greater than the last page. Redirect if it is.
|
||||
*/
|
||||
private function redirectOutOfBoundsPage(LengthAwarePaginator $mods): void
|
||||
{
|
||||
if ($mods->currentPage() > $mods->lastPage()) {
|
||||
$this->redirectRoute('mods', ['page' => $mods->lastPage()]);
|
||||
}
|
||||
|
||||
return view('livewire.mod.index', compact('mods'));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,11 +153,8 @@ class Index extends Component
|
||||
public function resetFilters(): void
|
||||
{
|
||||
$this->query = '';
|
||||
$this->sptVersions = $this->getLatestMinorVersions()->pluck('version')->toArray();
|
||||
$this->sptVersions = $this->getDefaultSptVersions();
|
||||
$this->featured = 'include';
|
||||
|
||||
// Clear local storage
|
||||
$this->dispatch('clear-filters');
|
||||
}
|
||||
|
||||
/**
|
98
app/Livewire/Profile/ManageOAuthConnections.php
Normal file
98
app/Livewire/Profile/ManageOAuthConnections.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Profile;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class ManageOAuthConnections extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
/**
|
||||
* Store the current user.
|
||||
*/
|
||||
#[Locked]
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* Controls the confirmation modal visibility.
|
||||
*/
|
||||
public $confirmingConnectionDeletion = false;
|
||||
|
||||
/**
|
||||
* Stores the ID of the connection to be deleted.
|
||||
*/
|
||||
#[Locked]
|
||||
public $selectedConnectionId;
|
||||
|
||||
/**
|
||||
* Initializes the component by loading the user's OAuth connections.
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->setName('profile.manage-oauth-connections');
|
||||
|
||||
$this->user = auth()->user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the deletion confirmation.
|
||||
*/
|
||||
public function confirmConnectionDeletion($connectionId): void
|
||||
{
|
||||
$this->confirmingConnectionDeletion = true;
|
||||
$this->selectedConnectionId = $connectionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the selected OAuth connection.
|
||||
*/
|
||||
public function deleteConnection(): void
|
||||
{
|
||||
$connection = $this->user->oauthConnections()->find($this->selectedConnectionId);
|
||||
|
||||
// Ensure the user is authorized to delete the connection.
|
||||
$this->authorize('delete', $connection);
|
||||
|
||||
// The user must have a password set before removing an OAuth connection.
|
||||
if ($this->user->password === null) {
|
||||
$this->addError('password_required', __('You must set a password before removing an OAuth connection.'));
|
||||
$this->confirmingConnectionDeletion = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($connection) {
|
||||
$connection->delete();
|
||||
|
||||
$this->user->refresh();
|
||||
$this->confirmingConnectionDeletion = false;
|
||||
$this->selectedConnectionId = null;
|
||||
|
||||
session()->flash('status', __('OAuth connection removed successfully.'));
|
||||
} else {
|
||||
session()->flash('error', __('OAuth connection not found.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the user instance.
|
||||
*/
|
||||
#[On('saved')]
|
||||
public function refreshUser(): void
|
||||
{
|
||||
$this->user->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the component view.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.profile.manage-oauth-connections');
|
||||
}
|
||||
}
|
52
app/Livewire/Profile/UpdatePasswordForm.php
Normal file
52
app/Livewire/Profile/UpdatePasswordForm.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Profile;
|
||||
|
||||
use App\Actions\Fortify\PasswordValidationRules;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||
use Laravel\Jetstream\Http\Livewire\UpdatePasswordForm as JetstreamUpdatePasswordForm;
|
||||
use Override;
|
||||
|
||||
class UpdatePasswordForm extends JetstreamUpdatePasswordForm
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*
|
||||
* This method has been overwritten to allow a user that has a null password to set a password for their account
|
||||
* without needing to provide their current password. This is useful for users that have been created using OAuth.
|
||||
*/
|
||||
#[Override]
|
||||
public function updatePassword(UpdatesUserPasswords $updater): void
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user->password !== null) {
|
||||
parent::updatePassword($updater);
|
||||
} else {
|
||||
|
||||
// User has a null password. Allow them to set a new password without their current password.
|
||||
Validator::make($this->state, [
|
||||
'password' => $this->passwordRules(),
|
||||
])->validateWithBag('updatePassword');
|
||||
|
||||
auth()->user()->forceFill([
|
||||
'password' => Hash::make($this->state['password']),
|
||||
])->save();
|
||||
|
||||
$this->state = [
|
||||
'current_password' => '',
|
||||
'password' => '',
|
||||
'password_confirmation' => '',
|
||||
];
|
||||
|
||||
$this->dispatch('saved');
|
||||
}
|
||||
}
|
||||
}
|
@ -12,8 +12,6 @@ class UpdateProfileForm extends UpdateProfileInformationForm
|
||||
{
|
||||
/**
|
||||
* The new cover photo for the user.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $cover;
|
||||
|
||||
|
52
app/Livewire/User/FollowButtons.php
Normal file
52
app/Livewire/User/FollowButtons.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\User;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
class FollowButtons extends Component
|
||||
{
|
||||
/**
|
||||
* The ID of the user whose profile is being viewed.
|
||||
*/
|
||||
#[Locked]
|
||||
public int $profileUserId;
|
||||
|
||||
/**
|
||||
* Whether the authenticated user is currently following the profile user.
|
||||
*/
|
||||
#[Locked]
|
||||
public bool $isFollowing;
|
||||
|
||||
/**
|
||||
* Action to follow a user.
|
||||
*/
|
||||
public function follow(): void
|
||||
{
|
||||
auth()->user()->follow($this->profileUserId);
|
||||
$this->isFollowing = true;
|
||||
|
||||
$this->dispatch('user-follow-change');
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to unfollow a user.
|
||||
*/
|
||||
public function unfollow(): void
|
||||
{
|
||||
auth()->user()->unfollow($this->profileUserId);
|
||||
$this->isFollowing = false;
|
||||
|
||||
$this->dispatch('user-follow-change');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.user.follow-buttons');
|
||||
}
|
||||
}
|
167
app/Livewire/User/FollowCard.php
Normal file
167
app/Livewire/User/FollowCard.php
Normal file
@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\User;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class FollowCard extends Component
|
||||
{
|
||||
/**
|
||||
* The type of user follow relationship to display.
|
||||
* Currently, either "followers" or "following".
|
||||
*/
|
||||
#[Locked]
|
||||
public string $relationship;
|
||||
|
||||
/**
|
||||
* The title of the card.
|
||||
*/
|
||||
public string $title;
|
||||
|
||||
/**
|
||||
* The message to display when there are no results.
|
||||
*/
|
||||
public string $emptyMessage;
|
||||
|
||||
/**
|
||||
* The title of the dialog.
|
||||
*/
|
||||
public string $dialogTitle;
|
||||
|
||||
/**
|
||||
* The user data to display in the card.
|
||||
*/
|
||||
#[Locked]
|
||||
public array $display = [];
|
||||
|
||||
/**
|
||||
* The limited user data to display in the card.
|
||||
*/
|
||||
#[Locked]
|
||||
public array $displayLimit = [];
|
||||
|
||||
/**
|
||||
* The maximum number of users to display on the card.
|
||||
*/
|
||||
#[Locked]
|
||||
public int $limit = 4;
|
||||
|
||||
/**
|
||||
* Whether to show all users in a model dialog.
|
||||
*/
|
||||
public bool $showFollowDialog = false;
|
||||
|
||||
/**
|
||||
* The user whose profile is being viewed.
|
||||
*/
|
||||
#[Locked]
|
||||
public User $profileUser;
|
||||
|
||||
/**
|
||||
* A collection of user IDs that the auth user follows.
|
||||
*/
|
||||
#[Locked]
|
||||
public Collection $authFollowIds;
|
||||
|
||||
/**
|
||||
* The profile user's followers (or following).
|
||||
*/
|
||||
#[Locked]
|
||||
public Collection $followUsers;
|
||||
|
||||
/**
|
||||
* The events the component should listen for.
|
||||
*/
|
||||
protected $listeners = ['refreshComponent' => '$refresh'];
|
||||
|
||||
/**
|
||||
* The number of users being displayed.
|
||||
*/
|
||||
#[Computed]
|
||||
public function followUsersCount(): int
|
||||
{
|
||||
return $this->followUsers->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the component is initialized.
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->setTitle();
|
||||
$this->setEmptyMessage();
|
||||
$this->setDialogTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title of the card based on the relationship.
|
||||
*/
|
||||
private function setTitle(): void
|
||||
{
|
||||
$this->title = match ($this->relationship) {
|
||||
'followers' => __('Followers'),
|
||||
'following' => __('Following'),
|
||||
default => __('Users'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the empty message based on the relationship.
|
||||
*/
|
||||
private function setEmptyMessage(): void
|
||||
{
|
||||
$this->emptyMessage = match ($this->relationship) {
|
||||
'followers' => __('No followers yet.'),
|
||||
'following' => __('Not yet following anyone.'),
|
||||
default => __('No users found.'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the dialog title based on the relationship.
|
||||
*/
|
||||
private function setDialogTitle(): void
|
||||
{
|
||||
$this->dialogTitle = match ($this->relationship) {
|
||||
'followers' => 'User :name has these followers:',
|
||||
'following' => 'User :name is following:',
|
||||
default => 'Users:',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
$this->populateFollowUsers();
|
||||
|
||||
return view('livewire.user.follow-card');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user follows or unfollows a user.
|
||||
*/
|
||||
#[On('auth-follow-change')]
|
||||
public function populateFollowUsers(): void
|
||||
{
|
||||
// Update the collection of profile user's followers (or following).
|
||||
$this->followUsers = $this->profileUser->{$this->relationship}()->get();
|
||||
|
||||
$this->dispatch('refreshComponent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle showing the follow dialog.
|
||||
*/
|
||||
public function toggleFollowDialog(): void
|
||||
{
|
||||
$this->showFollowDialog = ! $this->showFollowDialog;
|
||||
}
|
||||
}
|
51
app/Livewire/User/FollowCards.php
Normal file
51
app/Livewire/User/FollowCards.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\User;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class FollowCards extends Component
|
||||
{
|
||||
/**
|
||||
* The user account that is being viewed.
|
||||
*/
|
||||
#[Locked]
|
||||
public User $profileUser;
|
||||
|
||||
/**
|
||||
* A collection of user IDs that the auth user follows.
|
||||
*/
|
||||
#[Locked]
|
||||
public Collection $authFollowIds;
|
||||
|
||||
/**
|
||||
* Render the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
$this->updateAuthFollowIds();
|
||||
|
||||
return view('livewire.user.follow-cards');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user follows or unfollows a user.
|
||||
*/
|
||||
#[On('user-follow-change')]
|
||||
public function updateAuthFollowIds(): void
|
||||
{
|
||||
// Fetch IDs of all users the authenticated user is following.
|
||||
$this->authFollowIds = collect();
|
||||
$authUser = auth()->user();
|
||||
if ($authUser) {
|
||||
$this->authFollowIds = $authUser->following()->pluck('following_id');
|
||||
}
|
||||
|
||||
$this->dispatch('auth-follow-change');
|
||||
}
|
||||
}
|
@ -9,13 +9,30 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class License extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The relationship between a license and mod.
|
||||
*
|
||||
* @return HasMany<Mod>
|
||||
*/
|
||||
public function mods(): HasMany
|
||||
{
|
||||
return $this->hasMany(Mod::class);
|
||||
return $this->hasMany(Mod::class)
|
||||
->chaperone();
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'hub_id' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -14,28 +14,23 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
*/
|
||||
class Mod extends Model
|
||||
{
|
||||
use HasFactory, Searchable, SoftDeletes;
|
||||
use HasFactory;
|
||||
use Searchable;
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* Post boot method to configure the model.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
// Apply the global scope to exclude disabled mods.
|
||||
static::addGlobalScope(new DisabledScope);
|
||||
|
||||
// Apply the global scope to exclude non-published mods.
|
||||
static::addGlobalScope(new PublishedScope);
|
||||
}
|
||||
|
||||
@ -48,8 +43,24 @@ class Mod extends Model
|
||||
$this->saveQuietly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the URL to download the latest version of this mod.
|
||||
*/
|
||||
public function downloadUrl(bool $absolute = false): string
|
||||
{
|
||||
$this->load('latestVersion');
|
||||
|
||||
return route('mod.version.download', [
|
||||
$this->id,
|
||||
$this->slug,
|
||||
$this->latestVersion->version,
|
||||
], absolute: $absolute);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its users.
|
||||
*
|
||||
* @return BelongsToMany<User>
|
||||
*/
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
@ -58,30 +69,40 @@ class Mod extends Model
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its license.
|
||||
*
|
||||
* @return BelongsTo<License, Mod>
|
||||
*/
|
||||
public function license(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(License::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its last updated version.
|
||||
*
|
||||
* @return HasOne<ModVersion>
|
||||
*/
|
||||
public function latestUpdatedVersion(): HasOne
|
||||
{
|
||||
return $this->versions()
|
||||
->one()
|
||||
->ofMany('updated_at', 'max')
|
||||
->chaperone();
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its versions.
|
||||
*
|
||||
* @return HasMany<ModVersion>
|
||||
*/
|
||||
public function versions(): HasMany
|
||||
{
|
||||
return $this->hasMany(ModVersion::class)
|
||||
->whereHas('latestSptVersion')
|
||||
->orderByDesc('version');
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its last updated version.
|
||||
*/
|
||||
public function lastUpdatedVersion(): HasOne
|
||||
{
|
||||
return $this->hasOne(ModVersion::class)
|
||||
->whereHas('latestSptVersion')
|
||||
->orderByDesc('updated_at');
|
||||
->orderByDesc('version_major')
|
||||
->orderByDesc('version_minor')
|
||||
->orderByDesc('version_patch')
|
||||
->orderByDesc('version_pre_release')
|
||||
->chaperone();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,6 +110,11 @@ class Mod extends Model
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
$this->load([
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion',
|
||||
]);
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
@ -99,23 +125,11 @@ class Mod extends Model
|
||||
'created_at' => strtotime($this->created_at),
|
||||
'updated_at' => strtotime($this->updated_at),
|
||||
'published_at' => strtotime($this->published_at),
|
||||
'latestVersion' => $this->latestVersion()?->first()?->latestSptVersion()?->first()?->version_formatted,
|
||||
'latestVersionColorClass' => $this->latestVersion()?->first()?->latestSptVersion()?->first()?->color_class,
|
||||
'latestVersion' => $this->latestVersion->latestSptVersion->version_formatted,
|
||||
'latestVersionColorClass' => $this->latestVersion->latestSptVersion->color_class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship to the latest mod version, dictated by the mod version number.
|
||||
*/
|
||||
public function latestVersion(): HasOne
|
||||
{
|
||||
return $this->hasOne(ModVersion::class)
|
||||
->whereHas('sptVersions')
|
||||
->orderByDesc('version')
|
||||
->orderByDesc('updated_at')
|
||||
->take(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the model instance should be searchable.
|
||||
*/
|
||||
@ -131,16 +145,27 @@ class Mod extends Model
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch the latest version instance.
|
||||
$latestVersion = $this->latestVersion()?->first();
|
||||
// Eager load the latest mod version, and it's latest SPT version.
|
||||
$this->load([
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion',
|
||||
]);
|
||||
|
||||
// Ensure the mod has a latest version.
|
||||
if (is_null($latestVersion)) {
|
||||
if ($this->latestVersion()->doesntExist()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the latest version has a latest SPT version.
|
||||
if ($latestVersion->latestSptVersion()->doesntExist()) {
|
||||
if ($this->latestVersion->latestSptVersion()->doesntExist()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the latest SPT version is within the last three minor versions.
|
||||
$activeSptVersions = Cache::remember('active-spt-versions', 60 * 60, function () {
|
||||
return SptVersion::getVersionsForLastThreeMinors();
|
||||
});
|
||||
if (! in_array($this->latestVersion->latestSptVersion->version, $activeSptVersions->pluck('version')->toArray())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -148,6 +173,24 @@ class Mod extends Model
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its latest version.
|
||||
*
|
||||
* @return HasOne<ModVersion>
|
||||
*/
|
||||
public function latestVersion(): HasOne
|
||||
{
|
||||
return $this->versions()
|
||||
->one()
|
||||
->ofMany([
|
||||
'version_major' => 'max',
|
||||
'version_minor' => 'max',
|
||||
'version_patch' => 'max',
|
||||
'version_pre_release' => 'max',
|
||||
])
|
||||
->chaperone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the URL to the mod's thumbnail.
|
||||
*/
|
||||
@ -197,11 +240,17 @@ class Mod extends Model
|
||||
'contains_ai_content' => 'boolean',
|
||||
'contains_ads' => 'boolean',
|
||||
'disabled' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutate the slug attribute to always be lower case on get and slugified on set.
|
||||
*
|
||||
* @return Attribute<string, string>
|
||||
*/
|
||||
protected function slug(): Attribute
|
||||
{
|
||||
|
@ -7,19 +7,14 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $mod_version_id
|
||||
* @property int $dependency_mod_id
|
||||
* @property string $constraint
|
||||
* @property int|null $resolved_version_id
|
||||
*/
|
||||
class ModDependency extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The relationship between the mod dependency and the mod version.
|
||||
*
|
||||
* @return BelongsTo<ModVersion, ModDependency>
|
||||
*/
|
||||
public function modVersion(): BelongsTo
|
||||
{
|
||||
@ -28,17 +23,33 @@ class ModDependency extends Model
|
||||
|
||||
/**
|
||||
* The relationship between the mod dependency and the resolved dependency.
|
||||
*
|
||||
* @return HasMany<ModResolvedDependency>
|
||||
*/
|
||||
public function resolvedDependencies(): HasMany
|
||||
{
|
||||
return $this->hasMany(ModResolvedDependency::class, 'dependency_id');
|
||||
return $this->hasMany(ModResolvedDependency::class, 'dependency_id')
|
||||
->chaperone();
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between the mod dependency and the dependent mod.
|
||||
*
|
||||
* @return BelongsTo<Mod, ModDependency>
|
||||
*/
|
||||
public function dependentMod(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Mod::class, 'dependent_mod_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ class ModResolvedDependency extends Model
|
||||
{
|
||||
/**
|
||||
* The relationship between the resolved dependency and the mod version.
|
||||
*
|
||||
* @return BelongsTo<ModVersion, ModResolvedDependency>
|
||||
*/
|
||||
public function modVersion(): BelongsTo
|
||||
{
|
||||
@ -17,6 +19,8 @@ class ModResolvedDependency extends Model
|
||||
|
||||
/**
|
||||
* The relationship between the resolved dependency and the dependency.
|
||||
*
|
||||
* @return BelongsTo<ModDependency, ModResolvedDependency>
|
||||
*/
|
||||
public function dependency(): BelongsTo
|
||||
{
|
||||
@ -25,9 +29,22 @@ class ModResolvedDependency extends Model
|
||||
|
||||
/**
|
||||
* The relationship between the resolved dependency and the resolved mod version.
|
||||
*
|
||||
* @return BelongsTo<ModVersion, ModResolvedDependency>
|
||||
*/
|
||||
public function resolvedModVersion(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ModVersion::class, 'resolved_mod_version_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -2,23 +2,27 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\InvalidVersionNumberException;
|
||||
use App\Models\Scopes\DisabledScope;
|
||||
use App\Models\Scopes\PublishedScope;
|
||||
use App\Support\Version;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $mod_id
|
||||
* @property string $version
|
||||
*/
|
||||
class ModVersion extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* Update the parent mod's updated_at timestamp when the mod version is updated.
|
||||
*/
|
||||
protected $touches = ['mod'];
|
||||
|
||||
/**
|
||||
* Post boot method to configure the model.
|
||||
@ -26,11 +30,31 @@ class ModVersion extends Model
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::addGlobalScope(new DisabledScope);
|
||||
|
||||
static::addGlobalScope(new PublishedScope);
|
||||
|
||||
static::saving(function (ModVersion $model) {
|
||||
// Extract the version sections from the version string.
|
||||
try {
|
||||
$version = new Version($model->version);
|
||||
|
||||
$model->version_major = $version->getMajor();
|
||||
$model->version_minor = $version->getMinor();
|
||||
$model->version_patch = $version->getPatch();
|
||||
$model->version_pre_release = $version->getPreRelease();
|
||||
} catch (InvalidVersionNumberException $e) {
|
||||
$model->version_major = 0;
|
||||
$model->version_minor = 0;
|
||||
$model->version_patch = 0;
|
||||
$model->version_pre_release = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and mod.
|
||||
*
|
||||
* @return BelongsTo<Mod, ModVersion>
|
||||
*/
|
||||
public function mod(): BelongsTo
|
||||
{
|
||||
@ -39,14 +63,19 @@ class ModVersion extends Model
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and its dependencies.
|
||||
*
|
||||
* @return HasMany<ModDependency>
|
||||
*/
|
||||
public function dependencies(): HasMany
|
||||
{
|
||||
return $this->hasMany(ModDependency::class);
|
||||
return $this->hasMany(ModDependency::class)
|
||||
->chaperone();
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and its resolved dependencies.
|
||||
*
|
||||
* @return BelongsToMany<ModVersion>
|
||||
*/
|
||||
public function resolvedDependencies(): BelongsToMany
|
||||
{
|
||||
@ -57,6 +86,8 @@ class ModVersion extends Model
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and its each of it's resolved dependencies' latest versions.
|
||||
*
|
||||
* @return BelongsToMany<ModVersion>
|
||||
*/
|
||||
public function latestResolvedDependencies(): BelongsToMany
|
||||
{
|
||||
@ -71,22 +102,73 @@ class ModVersion extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and each of its SPT versions' latest version.
|
||||
* Hint: Be sure to call `->first()` on this to get the actual instance.
|
||||
* The relationship between a mod version and its latest SPT version.
|
||||
*
|
||||
* @return HasOneThrough<SptVersion>
|
||||
*/
|
||||
public function latestSptVersion(): BelongsToMany
|
||||
public function latestSptVersion(): HasOneThrough
|
||||
{
|
||||
return $this->belongsToMany(SptVersion::class, 'mod_version_spt_version')
|
||||
->orderBy('version', 'desc')
|
||||
return $this->hasOneThrough(SptVersion::class, ModVersionSptVersion::class, 'mod_version_id', 'id', 'id', 'spt_version_id')
|
||||
->orderByDesc('spt_versions.version_major')
|
||||
->orderByDesc('spt_versions.version_minor')
|
||||
->orderByDesc('spt_versions.version_patch')
|
||||
->orderByDesc('spt_versions.version_pre_release')
|
||||
->limit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and its SPT versions.
|
||||
*
|
||||
* @return BelongsToMany<SptVersion>
|
||||
*/
|
||||
public function sptVersions(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(SptVersion::class, 'mod_version_spt_version')
|
||||
->orderByDesc('version');
|
||||
return $this->belongsToMany(SptVersion::class)
|
||||
->using(ModVersionSptVersion::class)
|
||||
->orderByDesc('version_major')
|
||||
->orderByDesc('version_minor')
|
||||
->orderByDesc('version_patch')
|
||||
->orderByDesc('version_pre_release');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the download URL for this mod version.
|
||||
*/
|
||||
public function downloadUrl(bool $absolute = false): string
|
||||
{
|
||||
return route('mod.version.download', [$this->mod->id, $this->mod->slug, $this->version], absolute: $absolute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the download count for this mod version.
|
||||
*/
|
||||
public function incrementDownloads(): int
|
||||
{
|
||||
$this->downloads++;
|
||||
$this->save();
|
||||
|
||||
// Recalculate the total download count for this mod.
|
||||
$this->mod->calculateDownloads();
|
||||
|
||||
return $this->downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'hub_id' => 'integer',
|
||||
'version_major' => 'integer',
|
||||
'version_minor' => 'integer',
|
||||
'version_patch' => 'integer',
|
||||
'downloads' => 'integer',
|
||||
'disabled' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
10
app/Models/ModVersionSptVersion.php
Normal file
10
app/Models/ModVersionSptVersion.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
|
||||
class ModVersionSptVersion extends Pivot
|
||||
{
|
||||
public $incrementing = true;
|
||||
}
|
30
app/Models/OAuthConnection.php
Normal file
30
app/Models/OAuthConnection.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class OAuthConnection extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'oauth_connections';
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\InvalidVersionNumberException;
|
||||
use App\Support\Version;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@ -12,7 +13,8 @@ use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class SptVersion extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* Get all versions for the last three minor versions.
|
||||
@ -54,7 +56,7 @@ class SptVersion extends Model
|
||||
->orderByDesc('version_minor')
|
||||
->limit(3)
|
||||
->get()
|
||||
->map(function ($version) {
|
||||
->map(function (SptVersion $version) {
|
||||
return [
|
||||
'major' => (int) $version->version_major,
|
||||
'minor' => (int) $version->version_minor,
|
||||
@ -63,35 +65,6 @@ class SptVersion extends Model
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the model is booted.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
// Callback that runs before saving the model.
|
||||
static::saving(function ($model) {
|
||||
// Extract the version sections from the version string.
|
||||
if (! empty($model->version)) {
|
||||
// Default values in case there's an exception.
|
||||
$model->version_major = 0;
|
||||
$model->version_minor = 0;
|
||||
$model->version_patch = 0;
|
||||
$model->version_pre_release = '';
|
||||
|
||||
try {
|
||||
$versionSections = self::extractVersionSections($model->version);
|
||||
} catch (InvalidVersionNumberException $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$model->version_major = $versionSections['major'];
|
||||
$model->version_minor = $versionSections['minor'];
|
||||
$model->version_patch = $versionSections['patch'];
|
||||
$model->version_pre_release = $versionSections['pre_release'];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the version sections from the version string.
|
||||
*
|
||||
@ -116,6 +89,29 @@ class SptVersion extends Model
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the model is booted.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (SptVersion $model) {
|
||||
// Extract the version sections from the version string.
|
||||
try {
|
||||
$version = new Version($model->version);
|
||||
|
||||
$model->version_major = $version->getMajor();
|
||||
$model->version_minor = $version->getMinor();
|
||||
$model->version_patch = $version->getPatch();
|
||||
$model->version_pre_release = $version->getPreRelease();
|
||||
} catch (InvalidVersionNumberException $e) {
|
||||
$model->version_major = 0;
|
||||
$model->version_minor = 0;
|
||||
$model->version_patch = 0;
|
||||
$model->version_pre_release = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the mod count for this SptVersion.
|
||||
*/
|
||||
@ -131,10 +127,13 @@ class SptVersion extends Model
|
||||
|
||||
/**
|
||||
* The relationship between an SPT version and mod version.
|
||||
*
|
||||
* @return BelongsToMany<ModVersion>
|
||||
*/
|
||||
public function modVersions(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(ModVersion::class, 'mod_version_spt_version');
|
||||
return $this->belongsToMany(ModVersion::class)
|
||||
->using(ModVersionSptVersion::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,4 +172,21 @@ class SptVersion extends Model
|
||||
->first();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'hub_id' => 'integer',
|
||||
'version_major' => 'integer',
|
||||
'version_minor' => 'integer',
|
||||
'version_patch' => 'integer',
|
||||
'mod_count' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -8,9 +8,11 @@ use App\Notifications\VerifyEmail;
|
||||
use App\Traits\HasCoverPhoto;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Str;
|
||||
@ -42,14 +44,83 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
'profile_photo_url',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the storage path for profile photos.
|
||||
*/
|
||||
public static function profilePhotoStoragePath(): string
|
||||
{
|
||||
return 'profile-photos';
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a user and their mods.
|
||||
*
|
||||
* @return BelongsToMany<Mod>
|
||||
*/
|
||||
public function mods(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Mod::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a user and users that follow them.
|
||||
*
|
||||
* @return BelongsToMany<User>
|
||||
*/
|
||||
public function followers(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'user_follows', 'following_id', 'follower_id')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow another user.
|
||||
*/
|
||||
public function follow(User|int $user): void
|
||||
{
|
||||
$userId = $user instanceof User ? $user->id : $user;
|
||||
|
||||
if ($this->id === $userId) {
|
||||
// Don't allow following yourself.
|
||||
return;
|
||||
}
|
||||
|
||||
$this->following()->syncWithoutDetaching([$userId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a user and users they follow.
|
||||
*
|
||||
* @return BelongsToMany<User>
|
||||
*/
|
||||
public function following(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'user_follows', 'follower_id', 'following_id')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unfollow another user.
|
||||
*/
|
||||
public function unfollow(User|int $user): void
|
||||
{
|
||||
$userId = $user instanceof User ? $user->id : $user;
|
||||
|
||||
if ($this->isFollowing($userId)) {
|
||||
$this->following()->detach($userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is following another user.
|
||||
*/
|
||||
public function isFollowing(User|int $user): bool
|
||||
{
|
||||
$userId = $user instanceof User ? $user->id : $user;
|
||||
|
||||
return $this->following()->where('following_id', $userId)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* The data that is searchable by Scout.
|
||||
*/
|
||||
@ -66,7 +137,9 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
*/
|
||||
public function shouldBeSearchable(): bool
|
||||
{
|
||||
return ! is_null($this->email_verified_at);
|
||||
$this->load(['bans']);
|
||||
|
||||
return $this->isNotBanned();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,6 +205,8 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
|
||||
/**
|
||||
* The relationship between a user and their role.
|
||||
*
|
||||
* @return BelongsTo<UserRole, User>
|
||||
*/
|
||||
public function role(): BelongsTo
|
||||
{
|
||||
@ -146,6 +221,32 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
return $filters->apply($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a user and their OAuth providers.
|
||||
*/
|
||||
public function oAuthConnections(): HasMany
|
||||
{
|
||||
return $this->hasMany(OAuthConnection::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the about default value if empty. Thanks, MySQL!
|
||||
*/
|
||||
protected function about(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
// MySQL will not allow you to set a default value of an empty string for a (LONG)TEXT column. *le sigh*
|
||||
// NULL is the default. If NULL is saved, we'll swap it out for an empty string.
|
||||
if (is_null($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the disk that profile photos should be stored on.
|
||||
*/
|
||||
@ -160,8 +261,12 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'id' => 'integer',
|
||||
'hub_id' => 'integer',
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -12,9 +12,12 @@ class UserRole extends Model
|
||||
|
||||
/**
|
||||
* The relationship between a user role and users.
|
||||
*
|
||||
* @return HasMany<User>
|
||||
*/
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
return $this->hasMany(User::class)
|
||||
->chaperone();
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,9 @@ class ResetPassword extends OriginalResetPassword implements ShouldQueue
|
||||
parent::__construct($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [];
|
||||
|
@ -13,6 +13,9 @@ class VerifyEmail extends OriginalVerifyEmail implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [];
|
||||
|
@ -20,6 +20,8 @@ class ModObserver
|
||||
*/
|
||||
public function saved(Mod $mod): void
|
||||
{
|
||||
$mod->load('versions.sptVersions');
|
||||
|
||||
foreach ($mod->versions as $modVersion) {
|
||||
$this->dependencyVersionService->resolve($modVersion);
|
||||
}
|
||||
@ -44,6 +46,8 @@ class ModObserver
|
||||
*/
|
||||
public function deleted(Mod $mod): void
|
||||
{
|
||||
$mod->load('versions.sptVersions');
|
||||
|
||||
$this->updateRelatedSptVersions($mod);
|
||||
}
|
||||
}
|
||||
|
68
app/Policies/ModVersionPolicy.php
Normal file
68
app/Policies/ModVersionPolicy.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class ModVersionPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any mod versions.
|
||||
*/
|
||||
public function viewAny(?User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the mod version.
|
||||
*/
|
||||
public function view(?User $user, ModVersion $modVersion): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create mod versions.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the mod version.
|
||||
*/
|
||||
public function update(User $user, ModVersion $modVersion): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the mod version.
|
||||
*/
|
||||
public function delete(User $user, ModVersion $modVersion): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the mod version.
|
||||
*/
|
||||
public function restore(User $user, ModVersion $modVersion): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the mod version.
|
||||
*/
|
||||
public function forceDelete(User $user, ModVersion $modVersion): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
25
app/Policies/OAuthConnectionPolicy.php
Normal file
25
app/Policies/OAuthConnectionPolicy.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\OAuthConnection;
|
||||
use App\Models\User;
|
||||
|
||||
class OAuthConnectionPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, OAuthConnection $oauthConnection): bool
|
||||
{
|
||||
return $user->id === $oauthConnection->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, OAuthConnection $oauthConnection): bool
|
||||
{
|
||||
return $user->id === $oauthConnection->user_id && $user->password !== null;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Livewire\Profile\UpdatePasswordForm;
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModVersion;
|
||||
@ -11,11 +12,15 @@ use App\Observers\ModDependencyObserver;
|
||||
use App\Observers\ModObserver;
|
||||
use App\Observers\ModVersionObserver;
|
||||
use App\Observers\SptVersionObserver;
|
||||
use App\Services\LatestSptVersionService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Number;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Livewire\Livewire;
|
||||
use SocialiteProviders\Discord\Provider;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@ -24,9 +29,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(LatestSptVersionService::class, function ($app) {
|
||||
return new LatestSptVersionService;
|
||||
});
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
@ -37,18 +40,47 @@ class AppServiceProvider extends ServiceProvider
|
||||
// Allow mass assignment for all models. Be careful!
|
||||
Model::unguard();
|
||||
|
||||
// Register observers.
|
||||
Mod::observe(ModObserver::class);
|
||||
ModVersion::observe(ModVersionObserver::class);
|
||||
ModDependency::observe(ModDependencyObserver::class);
|
||||
SptVersion::observe(SptVersionObserver::class);
|
||||
// Disable lazy loading in non-production environments.
|
||||
Model::preventLazyLoading(! app()->isProduction());
|
||||
|
||||
// Register model observers.
|
||||
$this->registerObservers();
|
||||
|
||||
// Register custom macros.
|
||||
$this->registerNumberMacros();
|
||||
$this->registerCarbonMacros();
|
||||
|
||||
// Register Livewire component overrides.
|
||||
$this->registerLivewireOverrides();
|
||||
|
||||
// This gate determines who can access the Pulse dashboard.
|
||||
Gate::define('viewPulse', function (User $user) {
|
||||
return $user->isAdmin();
|
||||
});
|
||||
|
||||
// Register a number macro to format download numbers.
|
||||
// Register the Discord socialite provider.
|
||||
Event::listen(function (SocialiteWasCalled $event) {
|
||||
$event->extendSocialite('discord', Provider::class);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register model observers.
|
||||
*/
|
||||
private function registerObservers(): void
|
||||
{
|
||||
Mod::observe(ModObserver::class);
|
||||
ModVersion::observe(ModVersionObserver::class);
|
||||
ModDependency::observe(ModDependencyObserver::class);
|
||||
SptVersion::observe(SptVersionObserver::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom number macros.
|
||||
*/
|
||||
private function registerNumberMacros(): void
|
||||
{
|
||||
// Format download numbers.
|
||||
Number::macro('downloads', function (int|float $number) {
|
||||
return Number::forHumans(
|
||||
$number,
|
||||
@ -58,4 +90,30 @@ class AppServiceProvider extends ServiceProvider
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom Carbon macros.
|
||||
*/
|
||||
private function registerCarbonMacros(): void
|
||||
{
|
||||
// Format dates dynamically based on the time passed.
|
||||
Carbon::macro('dynamicFormat', function (Carbon $date) {
|
||||
if ($date->diff(now())->m > 1) {
|
||||
return $date->format('M jS, Y');
|
||||
}
|
||||
if ($date->diff(now())->d === 0) {
|
||||
return $date->diffForHumans();
|
||||
}
|
||||
|
||||
return $date->format('M jS, g:i A');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Livewire component overrides.
|
||||
*/
|
||||
private function registerLivewireOverrides(): void
|
||||
{
|
||||
Livewire::component('profile.update-password-form', UpdatePasswordForm::class);
|
||||
}
|
||||
}
|
||||
|
74
app/Support/Version.php
Normal file
74
app/Support/Version.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Exceptions\InvalidVersionNumberException;
|
||||
|
||||
class Version
|
||||
{
|
||||
protected int $major = 0;
|
||||
|
||||
protected int $minor = 0;
|
||||
|
||||
protected int $patch = 0;
|
||||
|
||||
protected string $preRelease = '';
|
||||
|
||||
protected string $version;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @throws InvalidVersionNumberException
|
||||
*/
|
||||
public function __construct(string $version)
|
||||
{
|
||||
$this->version = $version;
|
||||
$this->parseVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the version string into its components.
|
||||
*
|
||||
* @throws InvalidVersionNumberException
|
||||
*/
|
||||
protected function parseVersion(): void
|
||||
{
|
||||
$matches = [];
|
||||
|
||||
// Regex to match semantic versioning, including pre-release identifiers
|
||||
if (preg_match('/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([\w.-]+))?$/', $this->version, $matches)) {
|
||||
$this->major = (int) $matches[1];
|
||||
$this->minor = (int) ($matches[2] ?? 0);
|
||||
$this->patch = (int) ($matches[3] ?? 0);
|
||||
$this->preRelease = $matches[4] ?? '';
|
||||
} else {
|
||||
throw new InvalidVersionNumberException('Invalid version number: '.$this->version);
|
||||
}
|
||||
}
|
||||
|
||||
public function getMajor(): int
|
||||
{
|
||||
return $this->major;
|
||||
}
|
||||
|
||||
public function getMinor(): int
|
||||
{
|
||||
return $this->minor;
|
||||
}
|
||||
|
||||
public function getPatch(): int
|
||||
{
|
||||
return $this->patch;
|
||||
}
|
||||
|
||||
public function getPreRelease(): string
|
||||
{
|
||||
return $this->preRelease;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
}
|
@ -6,19 +6,32 @@ use Illuminate\Http\JsonResponse;
|
||||
|
||||
trait ApiResponses
|
||||
{
|
||||
/**
|
||||
* Return a success JSON response.
|
||||
*/
|
||||
protected function success(string $message, ?array $data = []): JsonResponse
|
||||
{
|
||||
return $this->baseResponse(message: $message, data: $data, code: 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* The base response.
|
||||
*/
|
||||
private function baseResponse(?string $message = '', ?array $data = [], ?int $code = 200): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
], $code);
|
||||
$response = [];
|
||||
$response['message'] = $message;
|
||||
if ($data) {
|
||||
$response['data'] = $data;
|
||||
}
|
||||
$response['status'] = $code;
|
||||
|
||||
return response()->json($response, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an error JSON response.
|
||||
*/
|
||||
protected function error(string $message, int $code): JsonResponse
|
||||
{
|
||||
return $this->baseResponse(message: $message, code: $code);
|
||||
|
@ -11,7 +11,7 @@ trait HasCoverPhoto
|
||||
/**
|
||||
* Update the user's cover photo.
|
||||
*/
|
||||
public function updateCoverPhoto(UploadedFile $cover, $storagePath = 'cover-photos'): void
|
||||
public function updateCoverPhoto(UploadedFile $cover, string $storagePath = 'cover-photos'): void
|
||||
{
|
||||
tap($this->cover_photo_path, function ($previous) use ($cover, $storagePath) {
|
||||
$this->forceFill([
|
||||
@ -51,15 +51,15 @@ trait HasCoverPhoto
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL to the user's cover photo.
|
||||
* Get the cover photo URL for the user.
|
||||
*/
|
||||
public function coverPhotoUrl(): Attribute
|
||||
{
|
||||
return Attribute::get(function (): string {
|
||||
return $this->cover_photo_path
|
||||
return new Attribute(
|
||||
get: fn (): string => $this->cover_photo_path
|
||||
? Storage::disk($this->coverPhotoDisk())->url($this->cover_photo_path)
|
||||
: $this->defaultCoverPhotoUrl();
|
||||
});
|
||||
: $this->defaultCoverPhotoUrl()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
75
app/Traits/V1/FilterMethods.php
Normal file
75
app/Traits/V1/FilterMethods.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\V1;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait FilterMethods
|
||||
{
|
||||
/**
|
||||
* Filter using a whereIn clause.
|
||||
*/
|
||||
public function filterWhereIn(string $column, string $value): Builder
|
||||
{
|
||||
$ids = array_map('trim', explode(',', $value));
|
||||
|
||||
return $this->builder->whereIn($column, $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter using a LIKE clause with a wildcard characters.
|
||||
*/
|
||||
public function filterByWildcardLike(string $column, string $value): Builder
|
||||
{
|
||||
$like = Str::replace('*', '%', $value);
|
||||
|
||||
return $this->builder->where($column, 'like', $like);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by date range or specific date.
|
||||
*/
|
||||
public function filterByDate(string $column, string $value): Builder
|
||||
{
|
||||
$dates = array_map('trim', explode(',', $value));
|
||||
|
||||
if (count($dates) > 1) {
|
||||
return $this->builder->whereBetween($column, $dates);
|
||||
}
|
||||
|
||||
return $this->builder->whereDate($column, $dates[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by boolean value.
|
||||
*/
|
||||
public function filterByBoolean(string $column, string $value): Builder
|
||||
{
|
||||
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
if ($value === null) {
|
||||
return $this->builder; // The unmodified builder
|
||||
}
|
||||
|
||||
return $this->builder->where($column, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the sort type to the query.
|
||||
*/
|
||||
protected function sort(string $values): Builder
|
||||
{
|
||||
$sortables = array_map('trim', explode(',', $values));
|
||||
|
||||
foreach ($sortables as $sortable) {
|
||||
$direction = Str::startsWith($sortable, '-') ? 'desc' : 'asc';
|
||||
$column = Str::of($sortable)->remove('-')->value();
|
||||
|
||||
if (in_array($column, $this->sortable)) {
|
||||
$this->builder->orderBy($column, $direction);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
}
|
88
app/View/Components/HomepageMods.php
Normal file
88
app/View/Components/HomepageMods.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use App\Models\Mod;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class HomepageMods extends Component
|
||||
{
|
||||
public function render(): View
|
||||
{
|
||||
return view('components.homepage-mods', [
|
||||
'featured' => [
|
||||
'title' => __('Featured Mods'),
|
||||
'mods' => $this->fetchFeaturedMods(),
|
||||
'link' => '/mods?featured=only',
|
||||
],
|
||||
'latest' => [
|
||||
'title' => __('Newest Mods'),
|
||||
'mods' => $this->fetchLatestMods(),
|
||||
'link' => '/mods',
|
||||
],
|
||||
'updated' => [
|
||||
'title' => __('Recently Updated Mods'),
|
||||
'mods' => $this->fetchUpdatedMods(),
|
||||
'link' => '/mods?order=updated',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the featured mods homepage listing.
|
||||
*/
|
||||
private function fetchFeaturedMods(): Collection
|
||||
{
|
||||
return Cache::flexible('homepage-featured-mods', [5, 10], function () {
|
||||
return Mod::whereFeatured(true)
|
||||
->with([
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->inRandomOrder()
|
||||
->limit(6)
|
||||
->get();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the latest mods homepage listing.
|
||||
*/
|
||||
private function fetchLatestMods(): Collection
|
||||
{
|
||||
return Cache::flexible('homepage-latest-mods', [5, 10], function () {
|
||||
return Mod::orderByDesc('created_at')
|
||||
->with([
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->limit(6)
|
||||
->get();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the recently updated mods homepage listing.
|
||||
*/
|
||||
private function fetchUpdatedMods(): Collection
|
||||
{
|
||||
return Cache::flexible('homepage-updated-mods', [5, 10], function () {
|
||||
return Mod::orderByDesc('updated_at')
|
||||
->with([
|
||||
'latestUpdatedVersion',
|
||||
'latestUpdatedVersion.latestSptVersion',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->limit(6)
|
||||
->get();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class ModList extends Component
|
||||
{
|
||||
public Collection $mods;
|
||||
|
||||
public string $versionScope;
|
||||
|
||||
public function __construct($mods, $versionScope)
|
||||
{
|
||||
$this->mods = $mods;
|
||||
$this->versionScope = $versionScope;
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('components.mod-list', [
|
||||
'mods' => $this->mods,
|
||||
'versionScope' => $this->versionScope,
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class ModListSection extends Component
|
||||
{
|
||||
public Collection $modsFeatured;
|
||||
|
||||
public Collection $modsLatest;
|
||||
|
||||
public Collection $modsUpdated;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->modsFeatured = $this->fetchFeaturedMods();
|
||||
$this->modsLatest = $this->fetchLatestMods();
|
||||
$this->modsUpdated = $this->fetchUpdatedMods();
|
||||
}
|
||||
|
||||
private function fetchFeaturedMods(): Collection
|
||||
{
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'downloads'])
|
||||
->with([
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion:id,version,color_class',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->whereFeatured(true)
|
||||
->inRandomOrder()
|
||||
->limit(6)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function fetchLatestMods(): Collection
|
||||
{
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at', 'downloads'])
|
||||
->with([
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion:id,version,color_class',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->latest()
|
||||
->limit(6)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function fetchUpdatedMods(): Collection
|
||||
{
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'downloads'])
|
||||
->with([
|
||||
'lastUpdatedVersion',
|
||||
'lastUpdatedVersion.latestSptVersion:id,version,color_class',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->joinSub(
|
||||
ModVersion::select('mod_id', DB::raw('MAX(updated_at) as latest_updated_at'))->groupBy('mod_id'),
|
||||
'latest_versions',
|
||||
'mods.id',
|
||||
'=',
|
||||
'latest_versions.mod_id'
|
||||
)
|
||||
->orderByDesc('latest_versions.latest_updated_at')
|
||||
->limit(6)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('components.mod-list-section', [
|
||||
'sections' => $this->getSections(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getSections(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'title' => __('Featured Mods'),
|
||||
'mods' => $this->modsFeatured,
|
||||
'versionScope' => 'latestVersion',
|
||||
],
|
||||
[
|
||||
'title' => __('Newest Mods'),
|
||||
'mods' => $this->modsLatest,
|
||||
'versionScope' => 'latestVersion',
|
||||
],
|
||||
[
|
||||
'title' => __('Recently Updated Mods'),
|
||||
'mods' => $this->modsUpdated,
|
||||
'versionScope' => 'lastUpdatedVersion',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class ModListStats extends Component
|
||||
{
|
||||
public function __construct(
|
||||
public $mod,
|
||||
public $modVersion
|
||||
) {}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('components.mod-list-stats');
|
||||
}
|
||||
}
|
@ -24,4 +24,5 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
//
|
||||
})->create();
|
||||
})
|
||||
->create();
|
||||
|
@ -6,4 +6,5 @@ return [
|
||||
App\Providers\HorizonServiceProvider::class,
|
||||
App\Providers\JetstreamServiceProvider::class,
|
||||
App\Providers\Filament\AdminPanelProvider::class,
|
||||
\SocialiteProviders\Manager\ServiceProvider::class,
|
||||
];
|
||||
|
@ -19,24 +19,26 @@
|
||||
"laravel/pulse": "^1.2",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/scout": "^10.10",
|
||||
"laravel/socialite": "^5.16",
|
||||
"laravel/tinker": "^2.9",
|
||||
"league/flysystem-aws-s3-v3": "^3.28",
|
||||
"league/html-to-markdown": "^5.1",
|
||||
"livewire/livewire": "^3.5",
|
||||
"mchev/banhammer": "^2.3",
|
||||
"meilisearch/meilisearch-php": "^1.8",
|
||||
"socialiteproviders/discord": "^4.2",
|
||||
"stevebauman/purify": "^6.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.13",
|
||||
"fakerphp/faker": "^1.23",
|
||||
"larastan/larastan": "^2.9",
|
||||
"knuckleswtf/scribe": "^4.37",
|
||||
"larastan/larastan": "^3.0",
|
||||
"laravel/pint": "^1.16",
|
||||
"laravel/sail": "^1.29",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.1",
|
||||
"pestphp/pest": "^2.34",
|
||||
"pestphp/pest-plugin-stressless": "^2.2",
|
||||
"nunomaduro/collision": "^8.4",
|
||||
"pestphp/pest": "^3.0",
|
||||
"spatie/laravel-ignition": "^2.8"
|
||||
},
|
||||
"autoload": {
|
||||
|
3130
composer.lock
generated
3130
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
@ -123,4 +125,19 @@ return [
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Class Aliases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This array of class aliases will be registered when this application
|
||||
| is started. However, feel free to register as many as you wish as
|
||||
| the aliases are "lazy" loaded so they don't hinder performance.
|
||||
|
|
||||
*/
|
||||
|
||||
'aliases' => Facade::defaultAliases()->merge([
|
||||
'Carbon' => \Carbon\Carbon::class,
|
||||
])->toArray(),
|
||||
|
||||
];
|
||||
|
@ -114,7 +114,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'inject_assets' => true,
|
||||
'inject_assets' => false,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
|
269
config/scribe.php
Normal file
269
config/scribe.php
Normal file
@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
use Knuckles\Scribe\Extracting\Strategies;
|
||||
|
||||
return [
|
||||
// The HTML <title> for the generated documentation. If this is empty, Scribe will infer it from config('app.name').
|
||||
'title' => 'The Forge API Documentation',
|
||||
|
||||
// A short description of your API. Will be included in the docs webpage, Postman collection and OpenAPI spec.
|
||||
'description' => '',
|
||||
|
||||
// The base URL displayed in the docs. If this is empty, Scribe will use the value of config('app.url') at generation time.
|
||||
// If you're using `laravel` type, you can set this to a dynamic string, like '{{ config("app.tenant_url") }}' to get a dynamic base URL.
|
||||
'base_url' => null,
|
||||
|
||||
'routes' => [
|
||||
[
|
||||
// Routes that match these conditions will be included in the docs
|
||||
'match' => [
|
||||
// Match only routes whose paths match this pattern (use * as a wildcard to match any characters). Example: 'users/*'.
|
||||
'prefixes' => ['api/*'],
|
||||
|
||||
// Match only routes whose domains match this pattern (use * as a wildcard to match any characters). Example: 'api.*'.
|
||||
'domains' => ['*'],
|
||||
|
||||
// [Dingo router only] Match only routes registered under this version. Wildcards are NOT supported.
|
||||
'versions' => ['v0'],
|
||||
],
|
||||
|
||||
// Include these routes even if they did not match the rules above.
|
||||
'include' => [
|
||||
// 'users.index', 'POST /new', '/auth/*'
|
||||
],
|
||||
|
||||
// Exclude these routes even if they matched the rules above.
|
||||
'exclude' => [
|
||||
// 'GET /health', 'admin.*'
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
// The type of documentation output to generate.
|
||||
// - "static" will generate a static HTMl page in the /public/docs folder,
|
||||
// - "laravel" will generate the documentation as a Blade view, so you can add routing and authentication.
|
||||
// - "external_static" and "external_laravel" do the same as above, but generate a basic template,
|
||||
// passing the OpenAPI spec as a URL, allowing you to easily use the docs with an external generator
|
||||
'type' => 'static',
|
||||
|
||||
// See https://scribe.knuckles.wtf/laravel/reference/config#theme for supported options
|
||||
'theme' => 'default',
|
||||
|
||||
'static' => [
|
||||
// HTML documentation, assets and Postman collection will be generated to this folder.
|
||||
// Source Markdown will still be in resources/docs.
|
||||
'output_path' => 'public/docs',
|
||||
],
|
||||
|
||||
'laravel' => [
|
||||
// Whether to automatically create a docs endpoint for you to view your generated docs.
|
||||
// If this is false, you can still set up routing manually.
|
||||
'add_routes' => true,
|
||||
|
||||
// URL path to use for the docs endpoint (if `add_routes` is true).
|
||||
// By default, `/docs` opens the HTML page, `/docs.postman` opens the Postman collection, and `/docs.openapi` the OpenAPI spec.
|
||||
'docs_url' => '/docs',
|
||||
|
||||
// Directory within `public` in which to store CSS and JS assets.
|
||||
// By default, assets are stored in `public/vendor/scribe`.
|
||||
// If set, assets will be stored in `public/{{assets_directory}}`
|
||||
'assets_directory' => null,
|
||||
|
||||
// Middleware to attach to the docs endpoint (if `add_routes` is true).
|
||||
'middleware' => [],
|
||||
],
|
||||
|
||||
'external' => [
|
||||
'html_attributes' => [],
|
||||
],
|
||||
|
||||
'try_it_out' => [
|
||||
// Add a Try It Out button to your endpoints so consumers can test endpoints right from their browser.
|
||||
// Don't forget to enable CORS headers for your endpoints.
|
||||
'enabled' => true,
|
||||
|
||||
// The base URL for the API tester to use (for example, you can set this to your staging URL).
|
||||
// Leave as null to use the current app URL when generating (config("app.url")).
|
||||
'base_url' => null,
|
||||
|
||||
// [Laravel Sanctum] Fetch a CSRF token before each request, and add it as an X-XSRF-TOKEN header.
|
||||
'use_csrf' => false,
|
||||
|
||||
// The URL to fetch the CSRF token from (if `use_csrf` is true).
|
||||
'csrf_url' => '/sanctum/csrf-cookie',
|
||||
],
|
||||
|
||||
// How is your API authenticated? This information will be used in the displayed docs, generated examples and response calls.
|
||||
'auth' => [
|
||||
// Set this to true if ANY endpoints in your API use authentication.
|
||||
'enabled' => true,
|
||||
|
||||
// Set this to true if your API should be authenticated by default. If so, you must also set `enabled` (above) to true.
|
||||
// You can then use @unauthenticated or @authenticated on individual endpoints to change their status from the default.
|
||||
'default' => true,
|
||||
|
||||
// Where is the auth value meant to be sent in a request?
|
||||
// Options: query, body, basic, bearer, header (for custom header)
|
||||
'in' => 'bearer',
|
||||
|
||||
// The name of the auth parameter (eg token, key, apiKey) or header (eg Authorization, Api-Key).
|
||||
'name' => 'Authorization',
|
||||
|
||||
// The value of the parameter to be used by Scribe to authenticate response calls.
|
||||
// This will NOT be included in the generated documentation. If empty, Scribe will use a random value.
|
||||
'use_value' => env('SCRIBE_AUTH_KEY'),
|
||||
|
||||
// Placeholder your users will see for the auth parameter in the example requests.
|
||||
// Set this to null if you want Scribe to use a random value as placeholder instead.
|
||||
'placeholder' => 'YOUR_API_KEY',
|
||||
|
||||
// Any extra authentication-related info for your users. Markdown and HTML are supported.
|
||||
'extra_info' => 'You can generate your own API token by logging into The Forge, clicking your profile picture, and clicking <b>API Tokens</b>.',
|
||||
],
|
||||
|
||||
// Text to place in the "Introduction" section, right after the `description`. Markdown and HTML are supported.
|
||||
'intro_text' => <<<'INTRO'
|
||||
This documentation aims to provide all the information you need to work with our API.
|
||||
|
||||
<aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).
|
||||
You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>
|
||||
INTRO
|
||||
,
|
||||
|
||||
// Example requests for each endpoint will be shown in each of these languages.
|
||||
// Supported options are: bash, javascript, php, python
|
||||
// To add a language of your own, see https://scribe.knuckles.wtf/laravel/advanced/example-requests
|
||||
'example_languages' => [
|
||||
'javascript',
|
||||
'php',
|
||||
'python',
|
||||
],
|
||||
|
||||
// Generate a Postman collection (v2.1.0) in addition to HTML docs.
|
||||
// For 'static' docs, the collection will be generated to public/docs/collection.json.
|
||||
// For 'laravel' docs, it will be generated to storage/app/scribe/collection.json.
|
||||
// Setting `laravel.add_routes` to true (above) will also add a route for the collection.
|
||||
'postman' => [
|
||||
'enabled' => true,
|
||||
'overrides' => [
|
||||
// 'info.version' => '2.0.0',
|
||||
],
|
||||
],
|
||||
|
||||
// Generate an OpenAPI spec (v3.0.1) in addition to docs webpage.
|
||||
// For 'static' docs, the collection will be generated to public/docs/openapi.yaml.
|
||||
// For 'laravel' docs, it will be generated to storage/app/scribe/openapi.yaml.
|
||||
// Setting `laravel.add_routes` to true (above) will also add a route for the spec.
|
||||
'openapi' => [
|
||||
'enabled' => true,
|
||||
'overrides' => [
|
||||
// 'info.version' => '2.0.0',
|
||||
],
|
||||
],
|
||||
|
||||
'groups' => [
|
||||
// Endpoints which don't have a @group will be placed in this default group.
|
||||
'default' => 'Endpoints',
|
||||
|
||||
// By default, Scribe will sort groups alphabetically, and endpoints in the order their routes are defined.
|
||||
// You can override this by listing the groups, subgroups and endpoints here in the order you want them.
|
||||
// See https://scribe.knuckles.wtf/blog/laravel-v4#easier-sorting and https://scribe.knuckles.wtf/laravel/reference/config#order for details
|
||||
'order' => [],
|
||||
],
|
||||
|
||||
// Custom logo path. This will be used as the value of the src attribute for the <img> tag,
|
||||
// so make sure it points to an accessible URL or path. Set to false to not use a logo.
|
||||
// For example, if your logo is in public/img:
|
||||
// - 'logo' => '../img/logo.png' // for `static` type (output folder is public/docs)
|
||||
// - 'logo' => 'img/logo.png' // for `laravel` type
|
||||
'logo' => false,
|
||||
|
||||
// Customize the "Last updated" value displayed in the docs by specifying tokens and formats.
|
||||
// Examples:
|
||||
// - {date:F j Y} => March 28, 2022
|
||||
// - {git:short} => Short hash of the last Git commit
|
||||
// Available tokens are `{date:<format>}` and `{git:<format>}`.
|
||||
// The format you pass to `date` will be passed to PHP's `date()` function.
|
||||
// The format you pass to `git` can be either "short" or "long".
|
||||
'last_updated' => 'Last updated: {date:F j, Y}',
|
||||
|
||||
'examples' => [
|
||||
// Set this to any number (eg. 1234) to generate the same example values for parameters on each run,
|
||||
'faker_seed' => null,
|
||||
|
||||
// With API resources and transformers, Scribe tries to generate example models to use in your API responses.
|
||||
// By default, Scribe will try the model's factory, and if that fails, try fetching the first from the database.
|
||||
// You can reorder or remove strategies here.
|
||||
'models_source' => ['factoryCreate', 'factoryMake', 'databaseFirst'],
|
||||
],
|
||||
|
||||
// The strategies Scribe will use to extract information about your routes at each stage.
|
||||
// If you create or install a custom strategy, add it here.
|
||||
'strategies' => [
|
||||
'metadata' => [
|
||||
Strategies\Metadata\GetFromDocBlocks::class,
|
||||
Strategies\Metadata\GetFromMetadataAttributes::class,
|
||||
],
|
||||
'urlParameters' => [
|
||||
Strategies\UrlParameters\GetFromLaravelAPI::class,
|
||||
Strategies\UrlParameters\GetFromUrlParamAttribute::class,
|
||||
Strategies\UrlParameters\GetFromUrlParamTag::class,
|
||||
],
|
||||
'queryParameters' => [
|
||||
Strategies\QueryParameters\GetFromFormRequest::class,
|
||||
Strategies\QueryParameters\GetFromInlineValidator::class,
|
||||
Strategies\QueryParameters\GetFromQueryParamAttribute::class,
|
||||
Strategies\QueryParameters\GetFromQueryParamTag::class,
|
||||
],
|
||||
'headers' => [
|
||||
Strategies\Headers\GetFromHeaderAttribute::class,
|
||||
Strategies\Headers\GetFromHeaderTag::class,
|
||||
[
|
||||
'override',
|
||||
[
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
],
|
||||
],
|
||||
'bodyParameters' => [
|
||||
Strategies\BodyParameters\GetFromFormRequest::class,
|
||||
Strategies\BodyParameters\GetFromInlineValidator::class,
|
||||
Strategies\BodyParameters\GetFromBodyParamAttribute::class,
|
||||
Strategies\BodyParameters\GetFromBodyParamTag::class,
|
||||
],
|
||||
'responses' => [
|
||||
Strategies\Responses\UseResponseAttributes::class,
|
||||
Strategies\Responses\UseTransformerTags::class,
|
||||
Strategies\Responses\UseApiResourceTags::class,
|
||||
Strategies\Responses\UseResponseTag::class,
|
||||
Strategies\Responses\UseResponseFileTag::class,
|
||||
[
|
||||
Strategies\Responses\ResponseCalls::class,
|
||||
[
|
||||
'only' => ['GET *'],
|
||||
// Disable debug mode when generating response calls to avoid error stack traces in responses
|
||||
'config' => [
|
||||
'app.debug' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'responseFields' => [
|
||||
Strategies\ResponseFields\GetFromResponseFieldAttribute::class,
|
||||
Strategies\ResponseFields\GetFromResponseFieldTag::class,
|
||||
],
|
||||
],
|
||||
|
||||
// For response calls, API resource responses and transformer responses,
|
||||
// Scribe will try to start database transactions, so no changes are persisted to your database.
|
||||
// Tell Scribe which connections should be transacted here. If you only use one db connection, you can leave this as is.
|
||||
'database_connections_to_transact' => [config('database.default')],
|
||||
|
||||
'fractal' => [
|
||||
// If you are using a custom serializer with league/fractal, you can specify it here.
|
||||
'serializer' => null,
|
||||
],
|
||||
|
||||
'routeMatcher' => \Knuckles\Scribe\Matching\RouteMatcher::class,
|
||||
];
|
@ -40,4 +40,12 @@ return [
|
||||
'token' => env('GITEA_TOKEN', ''),
|
||||
],
|
||||
|
||||
'discord' => [
|
||||
'client_id' => env('DISCORD_CLIENT_ID'),
|
||||
'client_secret' => env('DISCORD_CLIENT_SECRET'),
|
||||
'redirect' => env('DISCORD_REDIRECT_URI'),
|
||||
'allow_gif_avatars' => (bool) env('DISCORD_AVATAR_GIF', true),
|
||||
'avatar_default_extension' => env('DISCORD_EXTENSION_DEFAULT', 'webp'),
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -6,6 +6,9 @@ use App\Models\License;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @extends Factory<License>
|
||||
*/
|
||||
class LicenseFactory extends Factory
|
||||
{
|
||||
protected $model = License::class;
|
||||
|
@ -8,6 +8,9 @@ use App\Models\ModVersion;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @extends Factory<ModDependency>
|
||||
*/
|
||||
class ModDependencyFactory extends Factory
|
||||
{
|
||||
protected $model = ModDependency::class;
|
||||
|
@ -7,25 +7,23 @@ use App\Models\Mod;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use Random\RandomException;
|
||||
|
||||
/**
|
||||
* @extends Factory<Mod>
|
||||
*/
|
||||
class ModFactory extends Factory
|
||||
{
|
||||
protected $model = Mod::class;
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
|
||||
$name = fake()->catchPhrase();
|
||||
$name = fake()->sentence(rand(3, 5));
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'slug' => Str::slug($name),
|
||||
'teaser' => fake()->sentence(),
|
||||
'description' => fake()->paragraphs(random_int(4, 20), true),
|
||||
'description' => fake()->paragraphs(rand(4, 20), true),
|
||||
'license_id' => License::factory(),
|
||||
'source_code_link' => fake()->url(),
|
||||
'featured' => fake()->boolean(),
|
||||
|
@ -8,6 +8,9 @@ use App\Models\SptVersion;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @extends Factory<ModVersion>
|
||||
*/
|
||||
class ModVersionFactory extends Factory
|
||||
{
|
||||
protected $model = ModVersion::class;
|
||||
@ -16,7 +19,7 @@ class ModVersionFactory extends Factory
|
||||
{
|
||||
return [
|
||||
'mod_id' => Mod::factory(),
|
||||
'version' => fake()->numerify('#.#.#'),
|
||||
'version' => $this->faker->numerify('#.#.#'),
|
||||
'description' => fake()->text(),
|
||||
'link' => fake()->url(),
|
||||
|
||||
|
29
database/factories/OAuthConnectionFactory.php
Normal file
29
database/factories/OAuthConnectionFactory.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\OAuthConnection;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class OAuthConnectionFactory extends Factory
|
||||
{
|
||||
protected $model = OAuthConnection::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => User::factory(),
|
||||
'provider_name' => $this->faker->randomElement(['discord', 'google', 'facebook']),
|
||||
'provider_id' => (string) $this->faker->unique()->numberBetween(100000, 999999),
|
||||
'token' => Str::random(40),
|
||||
'refresh_token' => Str::random(40),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
@ -6,6 +6,9 @@ use App\Models\SptVersion;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @extends Factory<SptVersion>
|
||||
*/
|
||||
class SptVersionFactory extends Factory
|
||||
{
|
||||
protected $model = SptVersion::class;
|
||||
|
@ -2,10 +2,15 @@
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Random\RandomException;
|
||||
|
||||
/**
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
@ -13,8 +18,12 @@ class UserFactory extends Factory
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
protected $model = User::class;
|
||||
|
||||
/**
|
||||
* Define the user's default state.
|
||||
*
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
@ -23,6 +32,10 @@ class UserFactory extends Factory
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
|
||||
// TODO: Does Faker have a markdown plugin?
|
||||
'about' => fake()->paragraphs(random_int(1, 10), true),
|
||||
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
'remember_token' => Str::random(10),
|
||||
|
@ -6,6 +6,9 @@ use App\Models\UserRole;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @extends Factory<UserRole>
|
||||
*/
|
||||
class UserRoleFactory extends Factory
|
||||
{
|
||||
protected $model = UserRole::class;
|
||||
|
@ -14,14 +14,13 @@ return new class extends Migration
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('hub_id')
|
||||
->nullable()
|
||||
->default(null)
|
||||
->unique();
|
||||
$table->bigInteger('hub_id')->nullable()->default(null)->unique();
|
||||
$table->unsignedBigInteger('discord_id')->nullable()->default(null)->unique();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->string('password')->nullable();
|
||||
$table->longText('about')->nullable()->default(null);
|
||||
$table->foreignIdFor(UserRole::class)
|
||||
->nullable()
|
||||
->default(null)
|
||||
|
@ -23,7 +23,7 @@ return new class extends PulseMigration
|
||||
match ($this->driver()) {
|
||||
'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
|
||||
'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
|
||||
'sqlite' => $table->string('key_hash'),
|
||||
default => $table->string('key_hash'),
|
||||
};
|
||||
$table->mediumText('value');
|
||||
|
||||
@ -40,7 +40,7 @@ return new class extends PulseMigration
|
||||
match ($this->driver()) {
|
||||
'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
|
||||
'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
|
||||
'sqlite' => $table->string('key_hash'),
|
||||
default => $table->string('key_hash'),
|
||||
};
|
||||
$table->bigInteger('value')->nullable();
|
||||
|
||||
@ -59,7 +59,7 @@ return new class extends PulseMigration
|
||||
match ($this->driver()) {
|
||||
'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
|
||||
'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
|
||||
'sqlite' => $table->string('key_hash'),
|
||||
default => $table->string('key_hash'),
|
||||
};
|
||||
$table->string('aggregate');
|
||||
$table->decimal('value', 20, 2);
|
||||
|
@ -15,18 +15,17 @@ return new class extends Migration
|
||||
->default(null)
|
||||
->unique();
|
||||
$table->string('version');
|
||||
$table->unsignedInteger('version_major');
|
||||
$table->unsignedInteger('version_minor');
|
||||
$table->unsignedInteger('version_patch');
|
||||
$table->string('version_pre_release');
|
||||
$table->unsignedInteger('version_major')->default(0);
|
||||
$table->unsignedInteger('version_minor')->default(0);
|
||||
$table->unsignedInteger('version_patch')->default(0);
|
||||
$table->string('version_pre_release')->default('');
|
||||
$table->unsignedInteger('mod_count')->default(0);
|
||||
$table->string('link');
|
||||
$table->string('color_class');
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['version', 'deleted_at', 'id'], 'spt_versions_filtering_index');
|
||||
$table->index(['version_major', 'version_minor', 'version_patch', 'version_pre_release', 'deleted_at'], 'spt_versions_lookup_index');
|
||||
$table->index(['version', 'version_major', 'version_minor', 'version_patch', 'version_pre_release'], 'spt_versions_lookup_index');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,10 @@ return new class extends Migration
|
||||
->cascadeOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->string('version');
|
||||
$table->unsignedInteger('version_major')->default(0);
|
||||
$table->unsignedInteger('version_minor')->default(0);
|
||||
$table->unsignedInteger('version_patch')->default(0);
|
||||
$table->string('version_pre_release')->default('');
|
||||
$table->longText('description');
|
||||
$table->string('link');
|
||||
$table->string('spt_version_constraint');
|
||||
@ -32,6 +36,7 @@ return new class extends Migration
|
||||
|
||||
$table->index(['version']);
|
||||
$table->index(['mod_id', 'deleted_at', 'disabled', 'published_at'], 'mod_versions_filtering_index');
|
||||
$table->index(['version_major', 'version_minor', 'version_patch', 'version_pre_release'], 'mod_versions_version_components_index');
|
||||
$table->index(['id', 'deleted_at'], 'mod_versions_id_deleted_at_index');
|
||||
});
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ return new class extends Migration
|
||||
$table->id();
|
||||
$table->foreignId('mod_version_id')->constrained('mod_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->foreignId('spt_version_id')->constrained('spt_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['mod_version_id', 'spt_version_id'], 'mod_version_spt_version_index');
|
||||
$table->index(['spt_version_id', 'mod_version_id'], 'spt_version_mod_version_index');
|
||||
|
@ -0,0 +1,29 @@
|
||||
<?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('user_follows', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('follower_id')->references('id')->on('users')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->foreignId('following_id')->references('id')->on('users')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_follows');
|
||||
}
|
||||
};
|
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
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('oauth_connections', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(User::class)
|
||||
->constrained('users')
|
||||
->cascadeOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->string('provider');
|
||||
$table->string('provider_id');
|
||||
$table->string('token')->default('');
|
||||
$table->string('refresh_token')->default('');
|
||||
$table->string('nickname')->default('');
|
||||
$table->string('name')->default('');
|
||||
$table->string('email')->default('');
|
||||
$table->string('avatar')->default('');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['provider', 'provider_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('oauth_providers');
|
||||
}
|
||||
};
|
@ -6,10 +6,13 @@ use App\Models\License;
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use App\Models\User;
|
||||
use App\Models\UserRole;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Laravel\Prompts\Progress;
|
||||
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
@ -18,46 +21,114 @@ class DatabaseSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Create a few SPT versions.
|
||||
$spt_versions = SptVersion::factory(30)->create();
|
||||
// How many of each entity to create.
|
||||
$counts = [
|
||||
'license' => 10,
|
||||
'administrator' => 5,
|
||||
'moderator' => 5,
|
||||
'user' => 100,
|
||||
'mod' => 200,
|
||||
'modVersion' => 1500,
|
||||
];
|
||||
|
||||
// Create some code licenses.
|
||||
$licenses = License::factory(10)->create();
|
||||
// Licenses
|
||||
$licenses = License::factory($counts['license'])->create();
|
||||
|
||||
// Add 5 administrators.
|
||||
$administrator = UserRole::factory()->administrator()->create();
|
||||
User::factory()->for($administrator, 'role')->create([
|
||||
// Administrator Users
|
||||
$administratorRole = UserRole::factory()->administrator()->create();
|
||||
$testAccount = User::factory()->for($administratorRole, 'role')->create([
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
User::factory(4)->for($administrator, 'role')->create();
|
||||
User::factory($counts['administrator'] - 1)->for($administratorRole, 'role')->create();
|
||||
|
||||
// Add 10 moderators.
|
||||
$moderator = UserRole::factory()->moderator()->create();
|
||||
User::factory(5)->for($moderator, 'role')->create();
|
||||
$this->command->outputComponents()->info("Test account created: {$testAccount->email}");
|
||||
|
||||
// Add 100 users.
|
||||
$users = User::factory(100)->create();
|
||||
// Moderator Users
|
||||
$moderatorRole = UserRole::factory()->moderator()->create();
|
||||
User::factory($counts['moderator'])->for($moderatorRole, 'role')->create();
|
||||
|
||||
// Add 300 mods, assigning them to the users we just created.
|
||||
$allUsers = $users->merge([$administrator, $moderator]);
|
||||
$mods = Mod::factory(300)->recycle([$licenses])->create();
|
||||
foreach ($mods as $mod) {
|
||||
$userIds = $allUsers->random(rand(1, 3))->pluck('id')->toArray();
|
||||
$mod->users()->attach($userIds);
|
||||
}
|
||||
// Users
|
||||
progress(
|
||||
label: 'Adding Users...',
|
||||
steps: $counts['user'],
|
||||
callback: fn () => User::factory()->create()
|
||||
);
|
||||
|
||||
// Add 3000 mod versions, assigning them to the mods we just created.
|
||||
$modVersions = ModVersion::factory(3000)->recycle([$mods, $spt_versions])->create();
|
||||
// All Users
|
||||
$allUsers = User::all();
|
||||
|
||||
// Add ModDependencies to a subset of ModVersions.
|
||||
foreach ($modVersions as $modVersion) {
|
||||
$hasDependencies = rand(0, 100) < 30; // 30% chance to have dependencies
|
||||
if ($hasDependencies) {
|
||||
$dependencyMods = $mods->random(rand(1, 3)); // 1 to 3 dependencies
|
||||
// User Follows
|
||||
progress(
|
||||
label: 'adding user follows ...',
|
||||
steps: $allUsers,
|
||||
callback: function ($user) use ($allUsers) {
|
||||
$hasFollowers = rand(0, 100) < 70; // 70% chance to have followers
|
||||
$isFollowing = rand(0, 100) < 70; // 70% chance to be following other users
|
||||
|
||||
if ($hasFollowers) {
|
||||
$followers = $allUsers->random(rand(1, 10))->pluck('id')->toArray();
|
||||
$user->followers()->attach($followers);
|
||||
}
|
||||
|
||||
if ($isFollowing) {
|
||||
$following = $allUsers->random(rand(1, 10))->pluck('id')->toArray();
|
||||
$user->following()->attach($following);
|
||||
}
|
||||
});
|
||||
|
||||
// Mods
|
||||
$mods = collect(progress(
|
||||
label: 'Adding Mods...',
|
||||
steps: $counts['mod'],
|
||||
callback: fn () => Mod::factory()->recycle([$licenses])->create()
|
||||
));
|
||||
|
||||
// Attach users to mods
|
||||
progress(
|
||||
label: 'Attaching users to mods...',
|
||||
steps: $mods,
|
||||
callback: function (Mod $mod, Progress $progress) use ($allUsers) {
|
||||
$userIds = $allUsers->random(rand(1, 3))->pluck('id')->toArray();
|
||||
$mod->users()->attach($userIds);
|
||||
}
|
||||
);
|
||||
|
||||
// Add mod versions, assigning them to the mods we just created.
|
||||
$modVersions = collect(progress(
|
||||
label: 'Adding Mod Versions...',
|
||||
steps: $counts['modVersion'],
|
||||
callback: fn () => ModVersion::factory()->recycle([$mods])->create()
|
||||
));
|
||||
|
||||
// Add mod dependencies to *some* mod versions.
|
||||
progress(
|
||||
label: 'Adding Mod Dependencies...',
|
||||
steps: $modVersions,
|
||||
callback: function (ModVersion $modVersion, Progress $progress) use ($mods) {
|
||||
// 70% chance to not have dependencies
|
||||
if (rand(0, 9) >= 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Choose 1-3 random mods to be dependencies.
|
||||
$dependencyMods = $mods->random(rand(1, 3));
|
||||
foreach ($dependencyMods as $dependencyMod) {
|
||||
ModDependency::factory()->recycle([$modVersion, $dependencyMod])->create();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$this->command->outputComponents()->success('Initial seeding complete');
|
||||
|
||||
Artisan::call('app:search-sync');
|
||||
Artisan::call('app:resolve-versions');
|
||||
Artisan::call('app:count-mods');
|
||||
Artisan::call('app:update-downloads');
|
||||
$this->command->outputComponents()->warn('Jobs added to queue. Ensure Horizon is running!');
|
||||
|
||||
Artisan::call('cache:clear');
|
||||
$this->command->outputComponents()->info('Cache cleared');
|
||||
|
||||
$this->command->outputComponents()->success('Database seeding complete');
|
||||
}
|
||||
}
|
||||
|
451
package-lock.json
generated
451
package-lock.json
generated
@ -1,24 +1,24 @@
|
||||
{
|
||||
"name": "html",
|
||||
"name": "forge",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@alpinejs/focus": "^3.14.1"
|
||||
"@alpinejs/focus": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"axios": "^1.6.4",
|
||||
"chokidar": "^3.6.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"laravel-vite-plugin": "^1.0",
|
||||
"postcss": "^8.4.32",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"vite": "^5.0"
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"vite": "^5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@ -35,9 +35,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@alpinejs/focus": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@alpinejs/focus/-/focus-3.14.1.tgz",
|
||||
"integrity": "sha512-z4xdpK6X1LB2VitsWbL61tmABoOORuEhE5v2tnUX/be6/nAygXyeDxZ1x9s1u+bOEYlIOXXLmjdmTlhchUVWxw==",
|
||||
"version": "3.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@alpinejs/focus/-/focus-3.14.3.tgz",
|
||||
"integrity": "sha512-ZBL6HziMXhQIuta3PQjpYaMb5Ro9VPqh0mkP+d1uefJnhliBMWUfQXOnobV/0zJUB9pDxzd78diDX3ywewoJ3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"focus-trap": "^6.9.4",
|
||||
@ -556,9 +556,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz",
|
||||
"integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.4.tgz",
|
||||
"integrity": "sha512-2Y3JT6f5MrQkICUyRVCw4oa0sutfAsgaSsb0Lmmy1Wi2y7X5vT9Euqw4gOsCyy0YfKURBg35nhUKZS4mDcfULw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -570,9 +570,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz",
|
||||
"integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.4.tgz",
|
||||
"integrity": "sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -584,9 +584,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz",
|
||||
"integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.4.tgz",
|
||||
"integrity": "sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -598,9 +598,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz",
|
||||
"integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.4.tgz",
|
||||
"integrity": "sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -611,10 +611,38 @@
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.4.tgz",
|
||||
"integrity": "sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.4.tgz",
|
||||
"integrity": "sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz",
|
||||
"integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.4.tgz",
|
||||
"integrity": "sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -626,9 +654,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz",
|
||||
"integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.4.tgz",
|
||||
"integrity": "sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -640,9 +668,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz",
|
||||
"integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.4.tgz",
|
||||
"integrity": "sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -654,9 +682,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz",
|
||||
"integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.4.tgz",
|
||||
"integrity": "sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -668,9 +696,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz",
|
||||
"integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.4.tgz",
|
||||
"integrity": "sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -682,9 +710,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz",
|
||||
"integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.4.tgz",
|
||||
"integrity": "sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -696,9 +724,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz",
|
||||
"integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.4.tgz",
|
||||
"integrity": "sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@ -710,9 +738,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz",
|
||||
"integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz",
|
||||
"integrity": "sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -724,9 +752,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz",
|
||||
"integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.4.tgz",
|
||||
"integrity": "sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -738,9 +766,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz",
|
||||
"integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.4.tgz",
|
||||
"integrity": "sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -752,9 +780,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz",
|
||||
"integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.4.tgz",
|
||||
"integrity": "sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -766,9 +794,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz",
|
||||
"integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.4.tgz",
|
||||
"integrity": "sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -780,9 +808,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@tailwindcss/forms": {
|
||||
"version": "0.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.8.tgz",
|
||||
"integrity": "sha512-DJs7B7NPD0JH7BVvdHWNviWmunlFhuEkz7FyFxE4japOWYMLl9b1D6+Z9mivJJPWr6AEbmlPqgiFRyLwFB1SgQ==",
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz",
|
||||
"integrity": "sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -809,16 +837,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
||||
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -915,9 +943,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.6.tgz",
|
||||
"integrity": "sha512-Ekur6XDwhnJ5RgOCaxFnXyqlPALI3rVeukZMwOdfghW7/wGz784BYKiQq+QD8NPcr91KRo30KfHOchyijwWw7g==",
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -970,9 +998,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.23.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
|
||||
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
|
||||
"version": "4.24.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
|
||||
"integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -990,10 +1018,10 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001646",
|
||||
"electron-to-chromium": "^1.5.4",
|
||||
"caniuse-lite": "^1.0.30001669",
|
||||
"electron-to-chromium": "^1.5.41",
|
||||
"node-releases": "^2.0.18",
|
||||
"update-browserslist-db": "^1.1.0"
|
||||
"update-browserslist-db": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@ -1013,9 +1041,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001655",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz",
|
||||
"integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==",
|
||||
"version": "1.0.30001684",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz",
|
||||
"integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -1034,28 +1062,19 @@
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
|
||||
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
@ -1102,9 +1121,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1161,9 +1180,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.13",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz",
|
||||
"integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==",
|
||||
"version": "1.5.65",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.65.tgz",
|
||||
"integrity": "sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@ -1240,6 +1259,19 @@
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||
@ -1273,9 +1305,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -1311,9 +1343,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1386,16 +1418,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
"is-glob": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
@ -1517,9 +1549,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/laravel-vite-plugin": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.0.5.tgz",
|
||||
"integrity": "sha512-Zv+to82YLBknDCZ6g3iwOv9wZ7f6EWStb9pjSm7MGe9Mfoy5ynT2ssZbGsMr1udU6rDg9HOoYEVGw5Qf+p9zbw==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.0.6.tgz",
|
||||
"integrity": "sha512-B34OqmZc/rV1KvSjst8SsUm/LKHsuDusw8jiZCIhlnTHXbXnK89JUM9pTJuk6E/Vc/1DT2gX7qNfhipak1WS8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1743,9 +1775,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
|
||||
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
@ -1784,9 +1816,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@ -1824,9 +1856,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.41",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
|
||||
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
|
||||
"version": "8.4.49",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
|
||||
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -1845,8 +1877,8 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@ -2017,9 +2049,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.6.6",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.6.tgz",
|
||||
"integrity": "sha512-OPva5S7WAsPLEsOuOWXATi13QrCKACCiIonFgIR6V4lYv4QLp++UXVhZSzRbZxXGimkQtQT86CC6fQqTOybGng==",
|
||||
"version": "0.6.9",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.9.tgz",
|
||||
"integrity": "sha512-r0i3uhaZAXYP0At5xGfJH876W3HHGHDp+LCRUJrs57PBeQ6mYHMwr25KH8NPX44F2yGTvdnH7OqCshlQx183Eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -2134,16 +2166,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
|
||||
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
@ -2176,13 +2209,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz",
|
||||
"integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==",
|
||||
"version": "4.27.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.4.tgz",
|
||||
"integrity": "sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
"@types/estree": "1.0.6"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
@ -2192,22 +2225,24 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.21.2",
|
||||
"@rollup/rollup-android-arm64": "4.21.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.21.2",
|
||||
"@rollup/rollup-darwin-x64": "4.21.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.21.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.21.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.21.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.21.2",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.21.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.21.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.21.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.21.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.21.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.21.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.21.2",
|
||||
"@rollup/rollup-android-arm-eabi": "4.27.4",
|
||||
"@rollup/rollup-android-arm64": "4.27.4",
|
||||
"@rollup/rollup-darwin-arm64": "4.27.4",
|
||||
"@rollup/rollup-darwin-x64": "4.27.4",
|
||||
"@rollup/rollup-freebsd-arm64": "4.27.4",
|
||||
"@rollup/rollup-freebsd-x64": "4.27.4",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.27.4",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.27.4",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.27.4",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.27.4",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.27.4",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.27.4",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.27.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.27.4",
|
||||
"@rollup/rollup-linux-x64-musl": "4.27.4",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.27.4",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.27.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.27.4",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@ -2272,9 +2307,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
@ -2428,34 +2463,34 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
|
||||
"integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz",
|
||||
"integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"chokidar": "^3.6.0",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"fast-glob": "^3.3.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"jiti": "^1.21.0",
|
||||
"jiti": "^1.21.6",
|
||||
"lilconfig": "^2.1.0",
|
||||
"micromatch": "^4.0.5",
|
||||
"micromatch": "^4.0.8",
|
||||
"normalize-path": "^3.0.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "^8.4.23",
|
||||
"picocolors": "^1.1.1",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-js": "^4.0.1",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"postcss-nested": "^6.0.1",
|
||||
"postcss-selector-parser": "^6.0.11",
|
||||
"resolve": "^1.22.2",
|
||||
"sucrase": "^3.32.0"
|
||||
"postcss-load-config": "^4.0.2",
|
||||
"postcss-nested": "^6.2.0",
|
||||
"postcss-selector-parser": "^6.1.2",
|
||||
"resolve": "^1.22.8",
|
||||
"sucrase": "^3.35.0"
|
||||
},
|
||||
"bin": {
|
||||
"tailwind": "lib/cli.js",
|
||||
@ -2465,17 +2500,42 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"node_modules/tailwindcss/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/postcss-selector-parser": {
|
||||
@ -2492,6 +2552,19 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/thenify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
@ -2536,9 +2609,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
|
||||
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
|
||||
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -2556,8 +2629,8 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escalade": "^3.1.2",
|
||||
"picocolors": "^1.0.1"
|
||||
"escalade": "^3.2.0",
|
||||
"picocolors": "^1.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"update-browserslist-db": "cli.js"
|
||||
@ -2574,14 +2647,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
|
||||
"integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
||||
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.41",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
@ -2759,9 +2832,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
|
||||
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
|
||||
"integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user