Compare commits
49 Commits
v26.0.2
...
60c6718645
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60c6718645 | ||
| 225dc391ff | |||
| 16e0444fa3 | |||
| 6a2316c6f4 | |||
| 0b4e0df305 | |||
| 2a69ee8258 | |||
| 1c2353cfe1 | |||
| 496b50ed12 | |||
| 50e3ff6ded | |||
| fdf8d65310 | |||
| c2140b4493 | |||
| 652cf8bd6a | |||
| 5fdc0d45e3 | |||
| 6cde90042e | |||
| 942ab7858b | |||
| d178b8da91 | |||
| 7ecb6378fe | |||
| 9496078644 | |||
| 3aab864c34 | |||
| 5eb5404061 | |||
| d9040f1e6c | |||
| 8270e635d6 | |||
| d724f80cad | |||
| 1f5f340ce4 | |||
| 40e111b3a6 | |||
| 506011f933 | |||
| 80a8b86a08 | |||
| c1cb3f394a | |||
| 31c8491aaf | |||
| 0ad5916504 | |||
| bac70c3927 | |||
| bf23e46e2d | |||
| 55b9a69c42 | |||
| b6ce5160f9 | |||
| d279e7f36f | |||
| a0d914ea24 | |||
| ce3b89d54e | |||
| 5cd8a1a9d6 | |||
| 6f9d9f9e7a | |||
| db7f088b36 | |||
| 54d4cd7f99 | |||
| af03c23c9f | |||
| 68dd17f895 | |||
| 8249df15df | |||
| f167e64d00 | |||
| 95ebc7778d | |||
| c67a3ec6d0 | |||
| bf278667bc | |||
| 30a06e18f0 |
@@ -3,19 +3,49 @@ run-name: ${{ gitea.event.head_commit.message }}
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
jobs:
|
||||
test:
|
||||
runs-on: debian-latest
|
||||
stamp_build:
|
||||
if: gitea.ref_name == 'master' && !contains(gitea.event.head_commit.message, '[skip ci]')
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Show Debian version
|
||||
run: cat /etc/os-release
|
||||
- name: Test Deployment
|
||||
run: echo "Deployment test"
|
||||
- name: Stamp composer build from origin/master
|
||||
env:
|
||||
SPEEDBB_REPO: ${{ vars.SPEEDBB_REPO }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_ACTOR: ${{ gitea.actor }}
|
||||
run: |
|
||||
set -e
|
||||
REPO="$SPEEDBB_REPO"
|
||||
if [ -n "$GITEA_TOKEN" ]; then
|
||||
REPO=$(echo "$SPEEDBB_REPO" | sed "s#https://#https://${GITEA_ACTOR}:${GITEA_TOKEN}@#")
|
||||
fi
|
||||
|
||||
git clone --quiet --branch=master "$REPO" repo
|
||||
cd repo
|
||||
git fetch origin master
|
||||
|
||||
BUILD="$(git rev-list --count origin/master)"
|
||||
CURRENT="$(php -r 'echo (string) ((json_decode(file_get_contents("composer.json"), true)["build"] ?? ""));')"
|
||||
|
||||
if [ "$CURRENT" = "$BUILD" ]; then
|
||||
echo "composer.json build already $BUILD; no changes."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BUILD="$BUILD" php -r '$p="composer.json"; $d=json_decode(file_get_contents($p), true); if (!is_array($d)) { fwrite(STDERR, "Invalid composer.json\n"); exit(1);} $d["build"]=getenv("BUILD"); file_put_contents($p, json_encode($d, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES).PHP_EOL);'
|
||||
|
||||
git config user.name "speedbb-ci"
|
||||
git config user.email "ci@24unix.net"
|
||||
git add composer.json
|
||||
git commit -m "ci: sync composer build to ${BUILD} [skip ci]"
|
||||
git push origin master
|
||||
|
||||
deploy:
|
||||
if: gitea.ref_name == 'master' && !contains(gitea.event.head_commit.message, '[skip ci]')
|
||||
runs-on: self-hosted
|
||||
needs: test
|
||||
needs: stamp_build
|
||||
steps:
|
||||
- name: Custom Checkout
|
||||
env:
|
||||
@@ -37,3 +67,25 @@ jobs:
|
||||
echo "$ANSIBLE_VAULT_PASSWORD" > .vault_pass.txt
|
||||
ansible-playbook --vault-password-file .vault_pass.txt deploy-to-prod.yaml
|
||||
rm .vault_pass.txt
|
||||
|
||||
promote_stable:
|
||||
if: gitea.ref_name == 'master' && !contains(gitea.event.head_commit.message, '[skip ci]')
|
||||
runs-on: self-hosted
|
||||
needs: deploy
|
||||
steps:
|
||||
- name: Promote master to stable
|
||||
env:
|
||||
SPEEDBB_REPO: ${{ vars.SPEEDBB_REPO }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_ACTOR: ${{ gitea.actor }}
|
||||
run: |
|
||||
set -e
|
||||
REPO="$SPEEDBB_REPO"
|
||||
if [ -n "$GITEA_TOKEN" ]; then
|
||||
REPO=$(echo "$SPEEDBB_REPO" | sed "s#https://#https://${GITEA_ACTOR}:${GITEA_TOKEN}@#")
|
||||
fi
|
||||
git clone --quiet --depth=1 --branch=stable "$REPO" repo
|
||||
cd repo
|
||||
git fetch origin master
|
||||
git merge --ff-only FETCH_HEAD
|
||||
git push origin stable
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,12 +22,14 @@
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/custom
|
||||
/storage/app
|
||||
/storage/framework
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/storage/framework/views/*.php
|
||||
/bootstrap/cache/*.php
|
||||
/custom
|
||||
/vendor
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
|
||||
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,9 +1,45 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-24
|
||||
- Added login modal actions: `Cancel` button and accent-styled, right-aligned `Sign in` button.
|
||||
- Added functional `Forgot password?` flow with dedicated SPA route/page at `/reset-password`.
|
||||
- Implemented reset-link request UI wired to `POST /api/forgot-password`.
|
||||
- Implemented token-based new-password submission (`?token=...&email=...`) wired to `POST /api/reset-password`.
|
||||
- Updated reset flow UX to return to `/login` after successful reset-link request and after successful password update.
|
||||
- Added English and German translations for password reset screens/messages.
|
||||
- Added new `/ping` endpoint returning connection status, build status, and notification state.
|
||||
- Added frontend ping polling with active/hidden intervals and console diagnostics.
|
||||
- Added update-available detection comparing loaded build vs ping build, with footer refresh CTA.
|
||||
- Added update info modal prompting users to refresh when a newer build is detected.
|
||||
- Tuned global dark mode palette to reduce overly bright text/surfaces in dark theme.
|
||||
- Refined accent button state styling (hover/active/focus) to avoid Bootstrap blue fallback and preserve contrast.
|
||||
|
||||
## 2026-02-18
|
||||
- Added CLI default PHP version detection to system status (`php_default_version`) using the CLI `php` binary.
|
||||
- Updated ACP System -> CLI to show the CLI default PHP path/version in the panel header with sufficiency indicator and warning tooltip.
|
||||
- Simplified ACP CLI PHP selector to `php` or custom binary, and blocked saving `keyhelp-php-domain` from ACP.
|
||||
- Added test coverage expectation for `php_default_version` in system status unit tests.
|
||||
- Hardened `git_update.sh` PHP selection flow with clearer logging (`initial fallback`, `bootstrap read`, `final binary`).
|
||||
- Added strict PHP requirement enforcement in `git_update.sh` against `composer.json` and abort on insufficient CLI PHP.
|
||||
- Refactored `git_update.sh` to `main()` for source-safe testing and added Bats shell tests for resolver/requirement behavior.
|
||||
- Updated Gitea CI test job to install Bats and run `tests/shell/git_update.bats`.
|
||||
|
||||
## 2026-02-12
|
||||
- Refined ACP System tab with left navigation, section-specific requirements, and CLI PHP selector.
|
||||
- Added CLI PHP interpreter options (php, keyhelp-php-domain, custom) with KeyHelp guidance.
|
||||
- Updated CLI update tooling and automation notes (KeyHelp PHP handling, CI runner requirements).
|
||||
- Adjusted ACP layout and tab styling for better dark-mode readability and auto-sizing sidebars.
|
||||
- Added Custom top-level ACP tab and preserved /custom paths during in-app updates.
|
||||
|
||||
## 2026-02-10
|
||||
- Reshaped ACP System tab with left navigation and dedicated views (Overview, Live Update, CLI, CI/CD).
|
||||
- Moved system requirements table into the CI/CD view with refresh controls.
|
||||
|
||||
## 2026-02-08
|
||||
- Achieved 100% test coverage across the backend.
|
||||
- Added comprehensive Feature and Unit tests for controllers, models, services, and console commands.
|
||||
- Added extensive edge-case and error-path coverage (system update/status, versioning, attachments, forums, roles, ranks, settings, portal, etc.).
|
||||
- Added `git_update.sh` for CLI-based updates (stable branch, deps, build, migrations, version sync).
|
||||
|
||||
## 2026-01-12
|
||||
- Switched main SPA layouts to fluid containers to reduce wasted space.
|
||||
|
||||
8
NOTES.md
8
NOTES.md
@@ -1,7 +1,15 @@
|
||||
TODO: Remove remaining IIFEs in ACP UI; prefer plain components/helpers.
|
||||
Add git_update.sh script to update the forum and core.
|
||||
Tag the release as latest
|
||||
For update, make three tabs: insite, cli, ci/di and add explanation
|
||||
|
||||
Progress (last 2 days):
|
||||
- Reached 100% test coverage across the codebase.
|
||||
- Added extensive Feature and Unit tests for controllers, models, services, and console commands.
|
||||
- Added coverage scripts and cleanup (tests for update/version flows, system update/status, attachments, forums, roles, ranks, settings, portal, etc.).
|
||||
- Hardened tests with fakes/mocks to cover error paths and edge cases.
|
||||
|
||||
TODO: Make the PHP binary path configurable for updates if the default PHP is outdated (ACP -> System).
|
||||
CI/CD: Runner must have PHP 8.4+ as the default CLI interpreter.
|
||||
KeyHelp: `keyhelp-php-domain` can select the PHP version based on the domain of the script location.
|
||||
KeyHelp: `keyhelp-php-domain` is a Pro feature; on non-Pro setups we must fake the command.
|
||||
|
||||
@@ -21,12 +21,44 @@
|
||||
path: "{{ prod_base_dir }}/.env"
|
||||
register: env_file
|
||||
|
||||
- name: Ensure bootstrap cache directory exists
|
||||
file:
|
||||
path: "{{ prod_base_dir }}/bootstrap/cache"
|
||||
state: directory
|
||||
mode: "0775"
|
||||
|
||||
- name: Ensure public storage directory exists
|
||||
file:
|
||||
path: "{{ prod_base_dir }}/storage/app/public"
|
||||
state: directory
|
||||
mode: "0775"
|
||||
|
||||
- name: Ensure public storage symlink exists
|
||||
file:
|
||||
src: "{{ prod_base_dir }}/storage/app/public"
|
||||
dest: "{{ prod_base_dir }}/public/storage"
|
||||
state: link
|
||||
force: true
|
||||
|
||||
- name: Download and installs all libs and dependencies
|
||||
block:
|
||||
- name: Composer install
|
||||
community.general.composer:
|
||||
command: install
|
||||
arguments: --no-dev --optimize-autoloader
|
||||
working_dir: "{{ prod_base_dir }}"
|
||||
php_path: /usr/bin/keyhelp-php84
|
||||
rescue:
|
||||
- name: Debug package discovery
|
||||
shell: |
|
||||
keyhelp-php84 artisan package:discover -v --ansi 2>&1 | tail -n 200
|
||||
args:
|
||||
chdir: "{{ prod_base_dir }}"
|
||||
register: package_discover_debug
|
||||
- debug:
|
||||
var: package_discover_debug.stdout_lines
|
||||
- fail:
|
||||
msg: "Composer install failed; see package:discover output above."
|
||||
|
||||
- name: Install node_modules
|
||||
npm:
|
||||
@@ -74,8 +106,8 @@
|
||||
msg: "Database backed up to: {{ backup_result.stdout }}"
|
||||
when: env_file.stat.exists
|
||||
|
||||
- name: Run database migrations safely
|
||||
command: "keyhelp-php84 artisan migrate:safe --force"
|
||||
- name: Run database migrations
|
||||
command: "keyhelp-php84 artisan migrate --force"
|
||||
args:
|
||||
chdir: "{{ prod_base_dir }}"
|
||||
register: migrate_result
|
||||
|
||||
@@ -30,6 +30,10 @@ class BbcodeFormatter
|
||||
|
||||
private static function build(): array
|
||||
{
|
||||
if (app()->environment('testing') && env('BBCODE_FORCE_FAIL')) {
|
||||
throw new \RuntimeException('Unable to initialize BBCode formatter.');
|
||||
}
|
||||
|
||||
$configurator = new Configurator();
|
||||
$bbcodes = $configurator->plugins->load('BBCodes');
|
||||
$bbcodes->addFromRepository('B');
|
||||
|
||||
@@ -4,94 +4,75 @@ namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
class VersionFetch extends Command
|
||||
{
|
||||
protected $signature = 'version:fetch';
|
||||
|
||||
protected $description = 'Update the build number based on the git commit count of master.';
|
||||
protected $description = 'Sync version/build metadata into settings using composer.json as source of truth.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$version = Setting::where('key', 'version')->value('value');
|
||||
$build = $this->resolveBuildCount();
|
||||
|
||||
if ($version === null) {
|
||||
$this->error('Unable to determine version from settings.');
|
||||
$meta = $this->resolveComposerMetadata();
|
||||
if ($meta === null) {
|
||||
$this->error('Unable to determine version/build from composer.json.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($build === null) {
|
||||
$this->error('Unable to determine build number from git.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
$version = $meta['version'];
|
||||
$build = $meta['build'];
|
||||
|
||||
Setting::updateOrCreate(
|
||||
['key' => 'version'],
|
||||
['value' => $version],
|
||||
);
|
||||
|
||||
Setting::updateOrCreate(
|
||||
['key' => 'build'],
|
||||
['value' => (string) $build],
|
||||
);
|
||||
|
||||
if (!$this->syncComposerMetadata($version, $build)) {
|
||||
$this->error('Failed to sync version/build to composer.json.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info("Build number updated to {$build}.");
|
||||
$this->info("Version/build synced: {$version} (build {$build}).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function resolveBuildCount(): ?int
|
||||
{
|
||||
$commands = [
|
||||
['git', 'rev-list', '--count', 'master'],
|
||||
['git', 'rev-list', '--count', 'HEAD'],
|
||||
];
|
||||
|
||||
foreach ($commands as $command) {
|
||||
$process = new Process($command, base_path());
|
||||
$process->run();
|
||||
|
||||
if ($process->isSuccessful()) {
|
||||
$output = trim($process->getOutput());
|
||||
if (is_numeric($output)) {
|
||||
return (int) $output;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function syncComposerMetadata(string $version, int $build): bool
|
||||
private function resolveComposerMetadata(): ?array
|
||||
{
|
||||
$composerPath = base_path('composer.json');
|
||||
|
||||
if (!is_file($composerPath) || !is_readable($composerPath)) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
$raw = file_get_contents($composerPath);
|
||||
if ($raw === false) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
if (!is_array($data)) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
$data['version'] = $version;
|
||||
$data['build'] = (string) $build;
|
||||
$version = trim((string) ($data['version'] ?? ''));
|
||||
$buildRaw = trim((string) ($data['build'] ?? ''));
|
||||
|
||||
$encoded = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
if ($encoded === false) {
|
||||
return false;
|
||||
if ($version === '' || $buildRaw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$encoded .= "\n";
|
||||
if (!preg_match('/^\d+\.\d+(?:\.\d+)?(?:[-._][0-9A-Za-z.-]+)?$/', $version)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return file_put_contents($composerPath, $encoded) !== false;
|
||||
if (!ctype_digit($buildRaw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'version' => $version,
|
||||
'build' => (int) $buildRaw,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
23
app/Http/Controllers/PingController.php
Normal file
23
app/Http/Controllers/PingController.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class PingController extends Controller
|
||||
{
|
||||
public function __invoke(): JsonResponse
|
||||
{
|
||||
$build = Setting::query()->where('key', 'build')->value('value');
|
||||
$reportedBuild = $build !== null ? ((int) $build) + 1 : 1;
|
||||
|
||||
return response()->json([
|
||||
'connect' => 'ok',
|
||||
'version_status' => [
|
||||
'build' => $reportedBuild,
|
||||
],
|
||||
'notification_state' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\Process\Process;
|
||||
@@ -16,9 +17,13 @@ class SystemStatusController extends Controller
|
||||
}
|
||||
|
||||
$phpDefaultPath = $this->resolveBinary('php');
|
||||
$phpSelectedPath = PHP_BINARY ?: $phpDefaultPath;
|
||||
$phpDefaultVersion = $phpDefaultPath ? $this->resolvePhpVersion($phpDefaultPath) : null;
|
||||
$phpConfiguredPath = trim((string) Setting::where('key', 'system.php_binary')->value('value'));
|
||||
$phpSelectedPath = $phpConfiguredPath ?: (PHP_BINARY ?: $phpDefaultPath);
|
||||
$phpSelectedOk = (bool) $phpSelectedPath;
|
||||
$phpSelectedVersion = PHP_VERSION;
|
||||
$phpSelectedVersion = $phpSelectedPath
|
||||
? ($this->resolvePhpVersion($phpSelectedPath) ?? PHP_VERSION)
|
||||
: PHP_VERSION;
|
||||
$minVersions = $this->resolveMinVersions();
|
||||
$composerPath = $this->resolveBinary('composer');
|
||||
$nodePath = $this->resolveBinary('node');
|
||||
@@ -40,6 +45,8 @@ class SystemStatusController extends Controller
|
||||
return response()->json([
|
||||
'php' => PHP_VERSION,
|
||||
'php_default' => $phpDefaultPath,
|
||||
'php_default_version' => $phpDefaultVersion,
|
||||
'php_configured' => $phpConfiguredPath ?: null,
|
||||
'php_selected_path' => $phpSelectedPath,
|
||||
'php_selected_ok' => $phpSelectedOk,
|
||||
'php_selected_version' => $phpSelectedVersion,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\File;
|
||||
@@ -112,6 +113,7 @@ class SystemUpdateController extends Controller
|
||||
$append('Syncing files...');
|
||||
$usedRsync = false;
|
||||
$rsyncPath = trim((string) shell_exec('command -v rsync'));
|
||||
$protectedPaths = ['custom', 'public/custom'];
|
||||
if ($rsyncPath !== '') {
|
||||
$usedRsync = true;
|
||||
$rsync = new Process([
|
||||
@@ -121,6 +123,8 @@ class SystemUpdateController extends Controller
|
||||
'--exclude=.env',
|
||||
'--exclude=storage',
|
||||
'--exclude=public/storage',
|
||||
'--exclude=custom',
|
||||
'--exclude=public/custom',
|
||||
$sourceDir . '/',
|
||||
base_path() . '/',
|
||||
]);
|
||||
@@ -133,6 +137,15 @@ class SystemUpdateController extends Controller
|
||||
], 500);
|
||||
}
|
||||
} else {
|
||||
foreach ($protectedPaths as $path) {
|
||||
$sourcePath = $sourceDir . DIRECTORY_SEPARATOR . $path;
|
||||
if (File::exists($sourcePath)) {
|
||||
File::deleteDirectory($sourcePath);
|
||||
if (File::exists($sourcePath)) {
|
||||
File::delete($sourcePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
File::copyDirectory($sourceDir, base_path());
|
||||
}
|
||||
|
||||
@@ -169,7 +182,10 @@ class SystemUpdateController extends Controller
|
||||
], 500);
|
||||
}
|
||||
|
||||
$phpBinary = PHP_BINARY ?: 'php';
|
||||
$phpBinary = trim((string) Setting::where('key', 'system.php_binary')->value('value'));
|
||||
if ($phpBinary === '') {
|
||||
$phpBinary = env('SYSTEM_UPDATE_PHP_BINARY') ?: (PHP_BINARY ?: 'php');
|
||||
}
|
||||
$append("Running migrations (using {$phpBinary})...");
|
||||
$migrate = new Process([$phpBinary, 'artisan', 'migrate', '--force'], base_path());
|
||||
$migrate->setTimeout(600);
|
||||
|
||||
@@ -209,6 +209,10 @@ class UserController extends Controller
|
||||
->pluck('id')
|
||||
->all();
|
||||
$user->roles()->sync($roleIds);
|
||||
|
||||
if (in_array('ROLE_FOUNDER', $roleNames, true) && $user->email_verified_at === null) {
|
||||
$user->forceFill(['email_verified_at' => now()])->save();
|
||||
}
|
||||
}
|
||||
|
||||
$user->loadMissing('rank');
|
||||
|
||||
0
bootstrap/cache/.gitkeep
vendored
Normal file
0
bootstrap/cache/.gitkeep
vendored
Normal file
72
bootstrap/cache/packages.php
vendored
72
bootstrap/cache/packages.php
vendored
@@ -1,72 +0,0 @@
|
||||
<?php return array (
|
||||
'barryvdh/laravel-ide-helper' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
|
||||
),
|
||||
),
|
||||
'laravel/fortify' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Laravel\\Fortify\\FortifyServiceProvider',
|
||||
),
|
||||
),
|
||||
'laravel/pail' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Laravel\\Pail\\PailServiceProvider',
|
||||
),
|
||||
),
|
||||
'laravel/sail' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Laravel\\Sail\\SailServiceProvider',
|
||||
),
|
||||
),
|
||||
'laravel/sanctum' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Laravel\\Sanctum\\SanctumServiceProvider',
|
||||
),
|
||||
),
|
||||
'laravel/tinker' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Laravel\\Tinker\\TinkerServiceProvider',
|
||||
),
|
||||
),
|
||||
'nesbot/carbon' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Carbon\\Laravel\\ServiceProvider',
|
||||
),
|
||||
),
|
||||
'nunomaduro/collision' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
|
||||
),
|
||||
),
|
||||
'nunomaduro/termwind' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Termwind\\Laravel\\TermwindServiceProvider',
|
||||
),
|
||||
),
|
||||
'pestphp/pest-plugin-laravel' =>
|
||||
array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Pest\\Laravel\\PestServiceProvider',
|
||||
),
|
||||
),
|
||||
);
|
||||
277
bootstrap/cache/services.php
vendored
277
bootstrap/cache/services.php
vendored
@@ -1,277 +0,0 @@
|
||||
<?php return array (
|
||||
'providers' =>
|
||||
array (
|
||||
0 => 'Illuminate\\Auth\\AuthServiceProvider',
|
||||
1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
|
||||
2 => 'Illuminate\\Bus\\BusServiceProvider',
|
||||
3 => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||
4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
5 => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
|
||||
6 => 'Illuminate\\Cookie\\CookieServiceProvider',
|
||||
7 => 'Illuminate\\Database\\DatabaseServiceProvider',
|
||||
8 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
|
||||
9 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
|
||||
10 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
|
||||
11 => 'Illuminate\\Hashing\\HashServiceProvider',
|
||||
12 => 'Illuminate\\Mail\\MailServiceProvider',
|
||||
13 => 'Illuminate\\Notifications\\NotificationServiceProvider',
|
||||
14 => 'Illuminate\\Pagination\\PaginationServiceProvider',
|
||||
15 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
|
||||
16 => 'Illuminate\\Pipeline\\PipelineServiceProvider',
|
||||
17 => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||
18 => 'Illuminate\\Redis\\RedisServiceProvider',
|
||||
19 => 'Illuminate\\Session\\SessionServiceProvider',
|
||||
20 => 'Illuminate\\Translation\\TranslationServiceProvider',
|
||||
21 => 'Illuminate\\Validation\\ValidationServiceProvider',
|
||||
22 => 'Illuminate\\View\\ViewServiceProvider',
|
||||
23 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
|
||||
24 => 'Laravel\\Fortify\\FortifyServiceProvider',
|
||||
25 => 'Laravel\\Pail\\PailServiceProvider',
|
||||
26 => 'Laravel\\Sail\\SailServiceProvider',
|
||||
27 => 'Laravel\\Sanctum\\SanctumServiceProvider',
|
||||
28 => 'Laravel\\Tinker\\TinkerServiceProvider',
|
||||
29 => 'Carbon\\Laravel\\ServiceProvider',
|
||||
30 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
|
||||
31 => 'Termwind\\Laravel\\TermwindServiceProvider',
|
||||
32 => 'Pest\\Laravel\\PestServiceProvider',
|
||||
33 => 'App\\Providers\\AppServiceProvider',
|
||||
34 => 'App\\Providers\\FortifyServiceProvider',
|
||||
),
|
||||
'eager' =>
|
||||
array (
|
||||
0 => 'Illuminate\\Auth\\AuthServiceProvider',
|
||||
1 => 'Illuminate\\Cookie\\CookieServiceProvider',
|
||||
2 => 'Illuminate\\Database\\DatabaseServiceProvider',
|
||||
3 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
|
||||
4 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
|
||||
5 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
|
||||
6 => 'Illuminate\\Notifications\\NotificationServiceProvider',
|
||||
7 => 'Illuminate\\Pagination\\PaginationServiceProvider',
|
||||
8 => 'Illuminate\\Session\\SessionServiceProvider',
|
||||
9 => 'Illuminate\\View\\ViewServiceProvider',
|
||||
10 => 'Laravel\\Fortify\\FortifyServiceProvider',
|
||||
11 => 'Laravel\\Pail\\PailServiceProvider',
|
||||
12 => 'Laravel\\Sanctum\\SanctumServiceProvider',
|
||||
13 => 'Carbon\\Laravel\\ServiceProvider',
|
||||
14 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
|
||||
15 => 'Termwind\\Laravel\\TermwindServiceProvider',
|
||||
16 => 'Pest\\Laravel\\PestServiceProvider',
|
||||
17 => 'App\\Providers\\AppServiceProvider',
|
||||
18 => 'App\\Providers\\FortifyServiceProvider',
|
||||
),
|
||||
'deferred' =>
|
||||
array (
|
||||
'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
|
||||
'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
|
||||
'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
|
||||
'Illuminate\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||
'Illuminate\\Contracts\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||
'Illuminate\\Contracts\\Bus\\QueueingDispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||
'Illuminate\\Bus\\BatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||
'Illuminate\\Bus\\DatabaseBatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
|
||||
'cache' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||
'cache.store' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||
'cache.psr6' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||
'memcached.connector' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||
'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\AboutCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Cache\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Cache\\Console\\ForgetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ClearCompiledCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Auth\\Console\\ClearResetsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ConfigCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ConfigClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ConfigShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\DbCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\PruneCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\ShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\WipeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\DownCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EnvironmentCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EnvironmentDecryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EnvironmentEncryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EventCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EventClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EventListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Concurrency\\Console\\InvokeSerializedClosureCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\KeyGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\OptimizeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\OptimizeClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\PackageDiscoverCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Cache\\Console\\PruneStaleTagsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\ListFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\FlushFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\PauseCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\ResumeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ReloadCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\DumpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Seeds\\SeedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleFinishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleRunCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleClearCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleTestCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleWorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Console\\Scheduling\\ScheduleInterruptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\ShowModelCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\StorageLinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\StorageUnlinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\UpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ViewCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ViewClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ApiInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\BroadcastingInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Cache\\Console\\CacheTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\CastMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ChannelListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ChannelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ClassMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ComponentMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ConfigMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ConfigPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ConsoleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Routing\\Console\\ControllerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\DocsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EnumMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EventGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\EventMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ExceptionMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Factories\\FactoryMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\InterfaceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\JobMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\JobMiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\LangPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ListenerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\MailMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Routing\\Console\\MiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ModelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\NotificationMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Notifications\\Console\\NotificationTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ObserverMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\PolicyMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ProviderMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\FailedTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Queue\\Console\\BatchesTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\RequestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ResourceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\RuleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ScopeMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Seeds\\SeederMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Session\\Console\\SessionTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ServeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\StubPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\TestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\TraitMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\VendorPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Foundation\\Console\\ViewMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'migration.repository' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'migration.creator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Migrations\\Migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\MigrateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\FreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\InstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\RefreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\ResetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\RollbackCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\StatusCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Database\\Console\\Migrations\\MigrateMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'composer' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
|
||||
'Illuminate\\Concurrency\\ConcurrencyManager' => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
|
||||
'hash' => 'Illuminate\\Hashing\\HashServiceProvider',
|
||||
'hash.driver' => 'Illuminate\\Hashing\\HashServiceProvider',
|
||||
'mail.manager' => 'Illuminate\\Mail\\MailServiceProvider',
|
||||
'mailer' => 'Illuminate\\Mail\\MailServiceProvider',
|
||||
'Illuminate\\Mail\\Markdown' => 'Illuminate\\Mail\\MailServiceProvider',
|
||||
'auth.password' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
|
||||
'auth.password.broker' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
|
||||
'Illuminate\\Contracts\\Pipeline\\Hub' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
|
||||
'pipeline' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
|
||||
'queue' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||
'queue.connection' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||
'queue.failer' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||
'queue.listener' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||
'queue.worker' => 'Illuminate\\Queue\\QueueServiceProvider',
|
||||
'redis' => 'Illuminate\\Redis\\RedisServiceProvider',
|
||||
'redis.connection' => 'Illuminate\\Redis\\RedisServiceProvider',
|
||||
'translator' => 'Illuminate\\Translation\\TranslationServiceProvider',
|
||||
'translation.loader' => 'Illuminate\\Translation\\TranslationServiceProvider',
|
||||
'validator' => 'Illuminate\\Validation\\ValidationServiceProvider',
|
||||
'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider',
|
||||
'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider',
|
||||
'Barryvdh\\LaravelIdeHelper\\Console\\GeneratorCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
|
||||
'Barryvdh\\LaravelIdeHelper\\Console\\ModelsCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
|
||||
'Barryvdh\\LaravelIdeHelper\\Console\\MetaCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
|
||||
'Barryvdh\\LaravelIdeHelper\\Console\\EloquentCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
|
||||
'Laravel\\Sail\\Console\\InstallCommand' => 'Laravel\\Sail\\SailServiceProvider',
|
||||
'Laravel\\Sail\\Console\\PublishCommand' => 'Laravel\\Sail\\SailServiceProvider',
|
||||
'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider',
|
||||
),
|
||||
'when' =>
|
||||
array (
|
||||
'Illuminate\\Broadcasting\\BroadcastServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Bus\\BusServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Cache\\CacheServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Concurrency\\ConcurrencyServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Hashing\\HashServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Mail\\MailServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Pipeline\\PipelineServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Queue\\QueueServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Redis\\RedisServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Translation\\TranslationServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Illuminate\\Validation\\ValidationServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Laravel\\Sail\\SailServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
'Laravel\\Tinker\\TinkerServiceProvider' =>
|
||||
array (
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -97,5 +97,6 @@
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"version": "26.0.1"
|
||||
"version": "26.0.2",
|
||||
"build": "92"
|
||||
}
|
||||
|
||||
2
composer.lock
generated
2
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "83b577a02e99a4e17696941851d13cc2",
|
||||
"content-hash": "e6076a6989b155fddbc675cab28fdd50",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
|
||||
310
git_update.sh
Executable file
310
git_update.sh
Executable file
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2016
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
resolve_php_bin() {
|
||||
if [[ -n "${PHP_BIN:-}" ]]; then
|
||||
echo "$PHP_BIN"
|
||||
return
|
||||
fi
|
||||
if command -v keyhelp-php84 >/dev/null 2>&1; then
|
||||
echo "keyhelp-php84"
|
||||
return
|
||||
fi
|
||||
if command -v php >/dev/null 2>&1; then
|
||||
echo "php"
|
||||
return
|
||||
fi
|
||||
echo "php"
|
||||
}
|
||||
|
||||
resolve_configured_php_bin() {
|
||||
local configured="${1:-}"
|
||||
local current="${2:-php}"
|
||||
local trimmed="$configured"
|
||||
trimmed="${trimmed#"${trimmed%%[![:space:]]*}"}"
|
||||
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
|
||||
|
||||
if [[ -z "$trimmed" ]]; then
|
||||
echo "$current"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$trimmed" == "keyhelp-php-domain" ]]; then
|
||||
if command -v keyhelp-php-domain >/dev/null 2>&1; then
|
||||
echo "keyhelp-php-domain"
|
||||
return
|
||||
fi
|
||||
if [[ -x "/usr/bin/keyhelp-php-domain" ]]; then
|
||||
echo "/usr/bin/keyhelp-php-domain"
|
||||
return
|
||||
fi
|
||||
if [[ -x "/usr/local/bin/keyhelp-php-domain" ]]; then
|
||||
echo "/usr/local/bin/keyhelp-php-domain"
|
||||
return
|
||||
fi
|
||||
echo "Configured PHP binary 'keyhelp-php-domain' was not found." >&2
|
||||
echo "Set ACP -> System -> CLI to a working custom binary (e.g. keyhelp-php84)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v "$trimmed" >/dev/null 2>&1; then
|
||||
echo "$trimmed"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$trimmed" == */* && -x "$trimmed" ]]; then
|
||||
echo "$trimmed"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Configured PHP binary '$trimmed' is not executable/resolvable." >&2
|
||||
echo "Set ACP -> System -> CLI to a valid command or absolute executable path." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
read_setting_php_bin() {
|
||||
if [[ ! -f artisan ]]; then
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
echo "Using bootstrap PHP binary to read system.php_binary: $PHP_BIN" >&2
|
||||
"$PHP_BIN" -r '
|
||||
require "vendor/autoload.php";
|
||||
$app = require "bootstrap/app.php";
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
$value = (string) \App\Models\Setting::where("key", "system.php_binary")->value("value");
|
||||
echo trim($value);
|
||||
'
|
||||
}
|
||||
|
||||
enforce_php_requirement() {
|
||||
local bin="${1:-php}"
|
||||
echo "Validating PHP requirement from composer.json with binary: $bin"
|
||||
"$bin" -r '
|
||||
$composer = json_decode((string) file_get_contents("composer.json"), true);
|
||||
$constraint = (string) ($composer["require"]["php"] ?? "");
|
||||
$current = PHP_VERSION;
|
||||
|
||||
if ($constraint === "") {
|
||||
fwrite(STDOUT, "No PHP requirement found in composer.json; skipping check.\n");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$normalize = static function (string $value): ?array {
|
||||
if (!preg_match("/(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?/", $value, $m)) {
|
||||
return null;
|
||||
}
|
||||
return [(int) $m[1], (int) ($m[2] ?? 0), (int) ($m[3] ?? 0)];
|
||||
};
|
||||
|
||||
$cmp = static function (array $a, array $b): int {
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
if ($a[$i] > $b[$i]) {
|
||||
return 1;
|
||||
}
|
||||
if ($a[$i] < $b[$i]) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
$partMin = static function (string $part) use ($normalize): ?array {
|
||||
$tokens = preg_split("/\\s+/", trim($part)) ?: [];
|
||||
$tokens = array_values(array_filter($tokens, static fn ($t) => $t !== ""));
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if (str_starts_with($token, ">=")) {
|
||||
$parsed = $normalize(substr($token, 2));
|
||||
if ($parsed) {
|
||||
return $parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if (str_starts_with($token, "^")) {
|
||||
$parsed = $normalize(substr($token, 1));
|
||||
if ($parsed) {
|
||||
return $parsed;
|
||||
}
|
||||
}
|
||||
if (str_starts_with($token, "~")) {
|
||||
$parsed = $normalize(substr($token, 1));
|
||||
if ($parsed) {
|
||||
return $parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isset($tokens[0]) ? $normalize($tokens[0]) : null;
|
||||
};
|
||||
|
||||
$parts = array_values(array_filter(array_map("trim", explode("||", $constraint)), static fn ($p) => $p !== ""));
|
||||
$mins = [];
|
||||
foreach ($parts as $part) {
|
||||
$min = $partMin($part);
|
||||
if ($min) {
|
||||
$mins[] = $min;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$mins) {
|
||||
fwrite(STDOUT, "Could not parse PHP requirement \"$constraint\"; skipping strict check.\n");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$required = array_reduce($mins, static function ($carry, $item) use ($cmp) {
|
||||
if ($carry === null) {
|
||||
return $item;
|
||||
}
|
||||
return $cmp($item, $carry) < 0 ? $item : $carry;
|
||||
});
|
||||
|
||||
$currentParts = $normalize($current);
|
||||
if (!$currentParts) {
|
||||
fwrite(STDERR, "Unable to parse current PHP version: $current\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$requiredString = implode(".", $required);
|
||||
if ($cmp($currentParts, $required) < 0) {
|
||||
fwrite(STDERR, "PHP requirement check failed: composer.json requires \"$constraint\" (>= $requiredString), current is $current.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
fwrite(STDOUT, "PHP requirement check passed: composer.json requires \"$constraint\" (>= $requiredString), current is $current.\n");
|
||||
' || return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
git restore -q bootstrap/cache/packages.php bootstrap/cache/services.php 2>/dev/null || true
|
||||
DIRTY="$(git status --porcelain)"
|
||||
DIRTY_FILTERED="$(echo "$DIRTY" | grep -vE '^( M|M ) (bootstrap/cache/(packages|services)\.php|package-lock\.json)$' || true)"
|
||||
if [[ -n "$DIRTY_FILTERED" ]]; then
|
||||
echo "Working tree is dirty. Please commit or stash changes before updating."
|
||||
echo "$DIRTY_FILTERED"
|
||||
exit 1
|
||||
fi
|
||||
if echo "$DIRTY" | grep -qE 'package-lock\.json'; then
|
||||
echo "Warning: package-lock.json is modified. Continuing anyway."
|
||||
fi
|
||||
|
||||
echo "Fetching latest refs..."
|
||||
git fetch --prune --tags
|
||||
|
||||
echo "Checking out stable branch..."
|
||||
git checkout stable
|
||||
|
||||
echo "Pulling latest stable..."
|
||||
git pull --ff-only
|
||||
|
||||
PHP_BIN="$(resolve_php_bin)"
|
||||
echo "Initial fallback PHP binary: $PHP_BIN"
|
||||
if command -v "$PHP_BIN" >/dev/null 2>&1; then
|
||||
echo "PHP version ($PHP_BIN): $($PHP_BIN -v | head -n 1)"
|
||||
else
|
||||
echo "PHP binary '$PHP_BIN' not found in PATH."
|
||||
fi
|
||||
|
||||
echo "Installing PHP dependencies..."
|
||||
COMPOSER_BIN="$(command -v composer || true)"
|
||||
if [[ -z "$COMPOSER_BIN" ]]; then
|
||||
echo "Composer not found in PATH."
|
||||
exit 1
|
||||
fi
|
||||
echo "Running with PHP binary: $PHP_BIN $COMPOSER_BIN install --no-dev --optimize-autoloader"
|
||||
"$PHP_BIN" "$COMPOSER_BIN" install --no-dev --optimize-autoloader
|
||||
|
||||
if ! CONFIGURED_PHP="$(read_setting_php_bin)"; then
|
||||
echo "Failed to read configured PHP binary from settings." >&2
|
||||
echo "Aborting to avoid running update with the wrong PHP binary." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Configured PHP binary from settings: ${CONFIGURED_PHP:-<empty>}"
|
||||
PHP_BIN="$(resolve_configured_php_bin "$CONFIGURED_PHP" "$PHP_BIN")"
|
||||
|
||||
echo "Final PHP binary: $PHP_BIN"
|
||||
if command -v "$PHP_BIN" >/dev/null 2>&1; then
|
||||
echo "Final PHP version ($PHP_BIN): $($PHP_BIN -v | head -n 1)"
|
||||
fi
|
||||
if ! enforce_php_requirement "$PHP_BIN"; then
|
||||
echo "Aborting update because selected PHP binary does not satisfy composer.json requirements." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing JS dependencies..."
|
||||
npm install
|
||||
|
||||
echo "Building assets..."
|
||||
npm run build
|
||||
|
||||
echo "Running migrations..."
|
||||
echo "Running with PHP binary: $PHP_BIN artisan migrate --force"
|
||||
"$PHP_BIN" artisan migrate --force
|
||||
|
||||
echo "Syncing version/build to settings..."
|
||||
echo "Running with PHP binary: $PHP_BIN -r <read composer.json version>"
|
||||
VERSION="$("$PHP_BIN" -r '$c=json_decode(file_get_contents("composer.json"), true); echo $c["version"] ?? "";')"
|
||||
echo "Running with PHP binary: $PHP_BIN -r <read composer.json build>"
|
||||
BUILD="$("$PHP_BIN" -r '$c=json_decode(file_get_contents("composer.json"), true); echo $c["build"] ?? "";')"
|
||||
echo "Computed from composer.json: VERSION=$VERSION, BUILD=$BUILD"
|
||||
|
||||
if [[ -n "$VERSION" || -n "$BUILD" ]]; then
|
||||
echo "Updating settings version/build (VERSION=$VERSION, BUILD=$BUILD)..."
|
||||
echo "Running with PHP binary: $PHP_BIN -r <write settings version/build>"
|
||||
SPEEDBB_VERSION="$VERSION" SPEEDBB_BUILD="$BUILD" "$PHP_BIN" -r '
|
||||
require "vendor/autoload.php";
|
||||
$app = require "bootstrap/app.php";
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
$version = getenv("SPEEDBB_VERSION");
|
||||
$build = getenv("SPEEDBB_BUILD");
|
||||
if ($version !== false && $version !== "") {
|
||||
\Illuminate\Support\Facades\DB::table("settings")->upsert(
|
||||
[[
|
||||
"key" => "version",
|
||||
"value" => $version,
|
||||
"created_at" => now(),
|
||||
"updated_at" => now(),
|
||||
]],
|
||||
["key"],
|
||||
["value", "updated_at"]
|
||||
);
|
||||
echo "Upserted version setting.\n";
|
||||
}
|
||||
if ($build !== false && $build !== "") {
|
||||
\Illuminate\Support\Facades\DB::table("settings")->upsert(
|
||||
[[
|
||||
"key" => "build",
|
||||
"value" => $build,
|
||||
"created_at" => now(),
|
||||
"updated_at" => now(),
|
||||
]],
|
||||
["key"],
|
||||
["value", "updated_at"]
|
||||
);
|
||||
echo "Upserted build setting.\n";
|
||||
}
|
||||
' \
|
||||
&& echo "Running with PHP binary: $PHP_BIN -r <verify settings version/build>" \
|
||||
&& "$PHP_BIN" -r '
|
||||
require "vendor/autoload.php";
|
||||
$app = require "bootstrap/app.php";
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
$version = \App\Models\Setting::where("key", "version")->value("value");
|
||||
$build = \App\Models\Setting::where("key", "build")->value("value");
|
||||
echo "Settings now: version={$version}, build={$build}\n";
|
||||
'
|
||||
fi
|
||||
|
||||
echo "Update complete."
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
@@ -1,18 +1,19 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { BrowserRouter, Link, Route, Routes, useLocation } from 'react-router-dom'
|
||||
import { Container, NavDropdown } from 'react-bootstrap'
|
||||
import { Button, Container, Modal, NavDropdown } from 'react-bootstrap'
|
||||
import { AuthProvider, useAuth } from './context/AuthContext'
|
||||
import Home from './pages/Home'
|
||||
import ForumView from './pages/ForumView'
|
||||
import ThreadView from './pages/ThreadView'
|
||||
import Login from './pages/Login'
|
||||
import Register from './pages/Register'
|
||||
import ResetPassword from './pages/ResetPassword'
|
||||
import { Acp } from './pages/Acp'
|
||||
import BoardIndex from './pages/BoardIndex'
|
||||
import Ucp from './pages/Ucp'
|
||||
import Profile from './pages/Profile'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { fetchSettings, fetchVersion, getForum, getThread } from './api/client'
|
||||
import { fetchPing, fetchSettings, fetchVersion, getForum, getThread } from './api/client'
|
||||
|
||||
function PortalHeader({
|
||||
userMenu,
|
||||
@@ -240,9 +241,16 @@ function PortalHeader({
|
||||
}
|
||||
|
||||
function AppShell() {
|
||||
const PING_INTERVAL_MS = 15000
|
||||
const PING_INTERVAL_HIDDEN_MS = 60000
|
||||
const { t } = useTranslation()
|
||||
const { token, email, userId, logout, isAdmin, isModerator } = useAuth()
|
||||
const [versionInfo, setVersionInfo] = useState(null)
|
||||
const [availableBuild, setAvailableBuild] = useState(null)
|
||||
const [pingBuild, setPingBuild] = useState(null)
|
||||
const [showUpdateModal, setShowUpdateModal] = useState(false)
|
||||
const currentBuildRef = useRef(null)
|
||||
const promptedBuildRef = useRef(null)
|
||||
const [theme, setTheme] = useState('auto')
|
||||
const [resolvedTheme, setResolvedTheme] = useState('light')
|
||||
const [accentOverride, setAccentOverride] = useState(
|
||||
@@ -271,6 +279,73 @@ function AppShell() {
|
||||
.catch(() => setVersionInfo(null))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
currentBuildRef.current =
|
||||
typeof versionInfo?.build === 'number' ? versionInfo.build : null
|
||||
}, [versionInfo?.build])
|
||||
|
||||
useEffect(() => {
|
||||
const currentBuild =
|
||||
typeof versionInfo?.build === 'number' ? versionInfo.build : null
|
||||
if (currentBuild !== null && pingBuild !== null && pingBuild > currentBuild) {
|
||||
setAvailableBuild(pingBuild)
|
||||
return
|
||||
}
|
||||
setAvailableBuild(null)
|
||||
}, [versionInfo?.build, pingBuild])
|
||||
|
||||
useEffect(() => {
|
||||
if (availableBuild === null) return
|
||||
if (promptedBuildRef.current === availableBuild) return
|
||||
promptedBuildRef.current = availableBuild
|
||||
setShowUpdateModal(true)
|
||||
}, [availableBuild])
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
let timeoutId = null
|
||||
|
||||
const scheduleNext = () => {
|
||||
const delay = document.hidden ? PING_INTERVAL_HIDDEN_MS : PING_INTERVAL_MS
|
||||
timeoutId = window.setTimeout(runPing, delay)
|
||||
}
|
||||
|
||||
const runPing = async () => {
|
||||
try {
|
||||
const data = await fetchPing()
|
||||
const currentBuild = currentBuildRef.current
|
||||
const remoteBuild =
|
||||
typeof data?.version_status?.build === 'number'
|
||||
? data.version_status.build
|
||||
: null
|
||||
console.log('speedBB ping', {
|
||||
...data,
|
||||
current_version: currentBuild,
|
||||
})
|
||||
if (!active) return
|
||||
if (remoteBuild !== null) {
|
||||
setPingBuild(remoteBuild)
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('speedbb-ping', { detail: data }))
|
||||
} catch {
|
||||
// ignore transient ping failures
|
||||
} finally {
|
||||
if (active) {
|
||||
scheduleNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runPing()
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
if (timeoutId) {
|
||||
window.clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
const loadSettings = async () => {
|
||||
@@ -466,6 +541,7 @@ function AppShell() {
|
||||
<Route path="/forum/:id" element={<ForumView />} />
|
||||
<Route path="/thread/:id" element={<ThreadView />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/reset-password" element={<ResetPassword />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/profile/:id" element={<Profile />} />
|
||||
<Route path="/acp/*" element={<Acp isAdmin={isAdmin} />} />
|
||||
@@ -493,8 +569,35 @@ function AppShell() {
|
||||
<span className="bb-version-label">)</span>
|
||||
</span>
|
||||
)}
|
||||
{availableBuild !== null && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="bb-accent-button"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
{t('version.update_available_short')} (build {availableBuild}) ·{' '}
|
||||
{t('version.update_now')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
<Modal show={showUpdateModal} onHide={() => setShowUpdateModal(false)} centered>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{t('version.refresh_prompt_title')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{t('version.refresh_prompt_body', { build: availableBuild ?? '-' })}
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="justify-content-between">
|
||||
<Button variant="outline-secondary" onClick={() => setShowUpdateModal(false)}>
|
||||
{t('version.remind_later')}
|
||||
</Button>
|
||||
<Button className="bb-accent-button" onClick={() => window.location.reload()}>
|
||||
{t('version.update_now')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,20 @@ export async function registerUser({ email, username, plainPassword }) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function requestPasswordReset(email) {
|
||||
return apiFetch('/forgot-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function resetPassword({ token, email, password, password_confirmation }) {
|
||||
return apiFetch('/reset-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ token, email, password, password_confirmation }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function logoutUser() {
|
||||
return apiFetch('/logout', {
|
||||
method: 'POST',
|
||||
@@ -115,6 +129,15 @@ export async function fetchVersion() {
|
||||
return apiFetch('/version')
|
||||
}
|
||||
|
||||
export async function fetchPing() {
|
||||
const response = await fetch('/ping', {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
return parseResponse(response)
|
||||
}
|
||||
|
||||
export async function fetchVersionCheck() {
|
||||
return apiFetch('/version/check')
|
||||
}
|
||||
|
||||
@@ -862,10 +862,17 @@ a {
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
--bb-ink: #e6e8eb;
|
||||
--bb-ink-muted: #9aa4b2;
|
||||
--bb-border: #2a2f3a;
|
||||
--bb-page-bg: radial-gradient(circle at 10% 20%, #141823 0%, #10131a 45%, #0b0e14 100%);
|
||||
--bb-ink: #a3acb9;
|
||||
--bb-ink-muted: #626d7e;
|
||||
--bb-border: #242a35;
|
||||
--bb-page-bg: radial-gradient(circle at 10% 20%, #10151f 0%, #0b1018 45%, #080b11 100%);
|
||||
--bs-body-bg: #080b11;
|
||||
--bs-body-color: #b3bdca;
|
||||
--bs-secondary-bg: #121822;
|
||||
--bs-tertiary-bg: #0f141d;
|
||||
--bs-border-color: #242a35;
|
||||
--bs-modal-bg: #121822;
|
||||
--bs-modal-color: #b3bdca;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .bb-hero {
|
||||
@@ -875,11 +882,30 @@ a {
|
||||
|
||||
[data-bs-theme="dark"] .bb-card,
|
||||
[data-bs-theme="dark"] .bb-form {
|
||||
background: #171b22;
|
||||
border-color: #2a2f3a;
|
||||
background: #121822;
|
||||
border-color: #242a35;
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .modal-content {
|
||||
background: #121822;
|
||||
border-color: #242a35;
|
||||
color: #b3bdca;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .modal-content .modal-body,
|
||||
[data-bs-theme="dark"] .modal-content .modal-footer {
|
||||
background: #121822;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .modal-content .modal-header {
|
||||
color: #b7c0cc;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .modal-content .modal-title {
|
||||
color: #b7c0cc;
|
||||
}
|
||||
|
||||
[data-bs-theme="light"] .bb-forum-row {
|
||||
background: #fff;
|
||||
}
|
||||
@@ -922,10 +948,18 @@ a {
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
color: var(--bb-accent, #f29b3f);
|
||||
border: 1px solid var(--bb-border);
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 10px 10px 0 0;
|
||||
margin-right: 0.35rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: inherit;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: var(--bb-border);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.bb-version {
|
||||
@@ -2164,16 +2198,25 @@ a {
|
||||
}
|
||||
|
||||
.bb-accent-button {
|
||||
background: var(--bb-accent, #f29b3f);
|
||||
border-color: var(--bb-accent, #f29b3f);
|
||||
color: #0e121b;
|
||||
--bs-btn-bg: var(--bb-accent, #f29b3f);
|
||||
--bs-btn-border-color: var(--bb-accent, #f29b3f);
|
||||
--bs-btn-color: #0e121b;
|
||||
--bs-btn-hover-bg: color-mix(in srgb, var(--bb-accent, #f29b3f) 85%, #000);
|
||||
--bs-btn-hover-border-color: color-mix(in srgb, var(--bb-accent, #f29b3f) 85%, #000);
|
||||
--bs-btn-hover-color: #fff;
|
||||
--bs-btn-active-bg: color-mix(in srgb, var(--bb-accent, #f29b3f) 80%, #000);
|
||||
--bs-btn-active-border-color: color-mix(in srgb, var(--bb-accent, #f29b3f) 80%, #000);
|
||||
--bs-btn-active-color: #fff;
|
||||
--bs-btn-disabled-bg: var(--bb-accent, #f29b3f);
|
||||
--bs-btn-disabled-border-color: var(--bb-accent, #f29b3f);
|
||||
--bs-btn-disabled-color: #0e121b;
|
||||
background: var(--bs-btn-bg);
|
||||
border-color: var(--bs-btn-border-color);
|
||||
color: var(--bs-btn-color);
|
||||
}
|
||||
|
||||
.bb-accent-button:hover,
|
||||
.bb-accent-button:focus {
|
||||
background: color-mix(in srgb, var(--bb-accent, #f29b3f) 85%, #000);
|
||||
border-color: color-mix(in srgb, var(--bb-accent, #f29b3f) 85%, #000);
|
||||
color: #0e121b;
|
||||
.bb-accent-button:focus-visible {
|
||||
box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--bb-accent, #f29b3f) 35%, transparent);
|
||||
}
|
||||
|
||||
.bb-accent-button:disabled,
|
||||
|
||||
@@ -96,6 +96,7 @@ function Acp({ isAdmin }) {
|
||||
const [systemStatus, setSystemStatus] = useState(null)
|
||||
const [systemLoading, setSystemLoading] = useState(false)
|
||||
const [systemError, setSystemError] = useState('')
|
||||
const [systemSection, setSystemSection] = useState('info')
|
||||
const [usersPage, setUsersPage] = useState(1)
|
||||
const [usersPerPage, setUsersPerPage] = useState(10)
|
||||
const [userSort, setUserSort] = useState({ columnId: 'name', direction: 'asc' })
|
||||
@@ -200,6 +201,12 @@ function Acp({ isAdmin }) {
|
||||
favicon_128: '',
|
||||
favicon_256: '',
|
||||
})
|
||||
const [systemCliSettings, setSystemCliSettings] = useState({
|
||||
php_mode: 'php',
|
||||
php_custom: '',
|
||||
})
|
||||
const [systemCliSaving, setSystemCliSaving] = useState(false)
|
||||
const [systemCliError, setSystemCliError] = useState('')
|
||||
const settingsDetailMap = {
|
||||
forum_name: 'forumName',
|
||||
default_theme: 'defaultTheme',
|
||||
@@ -291,6 +298,12 @@ function Acp({ isAdmin }) {
|
||||
favicon_256: settingsMap.get('favicon_256') || '',
|
||||
}
|
||||
setGeneralSettings(next)
|
||||
const configuredPhp = settingsMap.get('system.php_binary') || ''
|
||||
const phpMode = configuredPhp === '' || configuredPhp === 'php' ? 'php' : 'custom'
|
||||
setSystemCliSettings({
|
||||
php_mode: phpMode,
|
||||
php_custom: phpMode === 'custom' ? configuredPhp : '',
|
||||
})
|
||||
setAttachmentSettings({
|
||||
display_images_inline: settingsMap.get('attachments.display_images_inline') || 'true',
|
||||
create_thumbnails: settingsMap.get('attachments.create_thumbnails') || 'true',
|
||||
@@ -372,6 +385,207 @@ function Acp({ isAdmin }) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSystemCliSave = async (event) => {
|
||||
event.preventDefault()
|
||||
setSystemCliSaving(true)
|
||||
setSystemCliError('')
|
||||
try {
|
||||
let value = ''
|
||||
if (systemCliSettings.php_mode === 'custom') {
|
||||
value = typeof systemCliSettings.php_custom === 'string'
|
||||
? systemCliSettings.php_custom.trim()
|
||||
: String(systemCliSettings.php_custom ?? '')
|
||||
} else {
|
||||
value = 'php'
|
||||
}
|
||||
if (value === 'keyhelp-php-domain') {
|
||||
throw new Error('`keyhelp-php-domain` is disabled in ACP CLI settings. Use a custom binary (e.g. keyhelp-php84).')
|
||||
}
|
||||
await saveSetting('system.php_binary', value)
|
||||
setSystemCliSettings((prev) => ({
|
||||
...prev,
|
||||
php_mode: systemCliSettings.php_mode,
|
||||
php_custom: systemCliSettings.php_mode === 'custom' ? value : '',
|
||||
}))
|
||||
} catch (err) {
|
||||
setSystemCliError(err.message)
|
||||
} finally {
|
||||
setSystemCliSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeSemver = (value) => {
|
||||
if (!value) return null
|
||||
const match = String(value).trim().match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
|
||||
if (!match) return null
|
||||
return [Number(match[1]), Number(match[2] || 0), Number(match[3] || 0)]
|
||||
}
|
||||
|
||||
const compareSemver = (a, b) => {
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
if (a[i] > b[i]) return 1
|
||||
if (a[i] < b[i]) return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const parseMinPhpConstraint = (constraint) => {
|
||||
if (!constraint) return null
|
||||
const parts = String(constraint)
|
||||
.split('||')
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
const mins = []
|
||||
|
||||
for (const part of parts) {
|
||||
const tokens = part.split(/\s+/).filter(Boolean)
|
||||
const geToken = tokens.find((token) => token.startsWith('>='))
|
||||
if (geToken) {
|
||||
const parsed = normalizeSemver(geToken.slice(2))
|
||||
if (parsed) mins.push(parsed)
|
||||
continue
|
||||
}
|
||||
|
||||
const caretToken = tokens.find((token) => token.startsWith('^'))
|
||||
if (caretToken) {
|
||||
const parsed = normalizeSemver(caretToken.slice(1))
|
||||
if (parsed) mins.push(parsed)
|
||||
continue
|
||||
}
|
||||
|
||||
const tildeToken = tokens.find((token) => token.startsWith('~'))
|
||||
if (tildeToken) {
|
||||
const parsed = normalizeSemver(tildeToken.slice(1))
|
||||
if (parsed) mins.push(parsed)
|
||||
continue
|
||||
}
|
||||
|
||||
const plain = normalizeSemver(tokens[0] || '')
|
||||
if (plain) mins.push(plain)
|
||||
}
|
||||
|
||||
if (!mins.length) return null
|
||||
return mins.reduce((lowest, current) => (compareSemver(current, lowest) < 0 ? current : lowest))
|
||||
}
|
||||
|
||||
const cliDefaultPhpIsSufficient = useMemo(() => {
|
||||
const minimum = parseMinPhpConstraint(systemStatus?.min_versions?.php)
|
||||
const current = normalizeSemver(systemStatus?.php_default_version)
|
||||
if (!minimum) return true
|
||||
if (!current) return false
|
||||
return compareSemver(current, minimum) >= 0
|
||||
}, [systemStatus])
|
||||
|
||||
const phpSelectedIsSufficient = useMemo(() => {
|
||||
if (!systemStatus?.php_selected_ok) return false
|
||||
const minimum = parseMinPhpConstraint(systemStatus?.min_versions?.php)
|
||||
const current = normalizeSemver(systemStatus?.php_selected_version)
|
||||
if (!minimum) return true
|
||||
if (!current) return false
|
||||
return compareSemver(current, minimum) >= 0
|
||||
}, [systemStatus])
|
||||
|
||||
const systemChecks = useMemo(() => {
|
||||
if (!systemStatus) return []
|
||||
return [
|
||||
{
|
||||
id: 'php',
|
||||
label: 'PHP',
|
||||
path: systemStatus.php_selected_path || '—',
|
||||
min: systemStatus.min_versions?.php || '—',
|
||||
current: systemStatus.php_selected_version || '—',
|
||||
status: phpSelectedIsSufficient ? 'ok' : 'bad',
|
||||
},
|
||||
{
|
||||
id: 'composer',
|
||||
label: 'Composer',
|
||||
path: systemStatus.composer || t('system.not_found'),
|
||||
min: systemStatus.min_versions?.composer || '—',
|
||||
current: systemStatus.composer_version || '—',
|
||||
status: systemStatus.composer ? 'ok' : 'bad',
|
||||
},
|
||||
{
|
||||
id: 'node',
|
||||
label: 'Node',
|
||||
path: systemStatus.node || t('system.not_found'),
|
||||
min: systemStatus.min_versions?.node || '—',
|
||||
current: systemStatus.node_version || '—',
|
||||
status: systemStatus.node ? 'ok' : 'bad',
|
||||
},
|
||||
{
|
||||
id: 'npm',
|
||||
label: 'npm',
|
||||
path: systemStatus.npm || t('system.not_found'),
|
||||
min: systemStatus.min_versions?.npm || '—',
|
||||
current: systemStatus.npm_version || '—',
|
||||
status: systemStatus.npm ? 'ok' : 'bad',
|
||||
},
|
||||
{
|
||||
id: 'tar',
|
||||
label: 'tar',
|
||||
path: systemStatus.tar || t('system.not_found'),
|
||||
min: '—',
|
||||
current: systemStatus.tar_version || '—',
|
||||
status: systemStatus.tar ? 'ok' : 'bad',
|
||||
},
|
||||
{
|
||||
id: 'rsync',
|
||||
label: 'rsync',
|
||||
path: systemStatus.rsync || t('system.not_found'),
|
||||
min: '—',
|
||||
current: systemStatus.rsync_version || '—',
|
||||
status: systemStatus.rsync ? 'ok' : 'bad',
|
||||
},
|
||||
{
|
||||
id: 'proc',
|
||||
label: 'proc_* functions',
|
||||
path: systemStatus.proc_functions
|
||||
? Object.entries(systemStatus.proc_functions)
|
||||
.filter(([, ok]) => !ok)
|
||||
.map(([name]) => name)
|
||||
.join(', ')
|
||||
: '—',
|
||||
min: '—',
|
||||
current: '—',
|
||||
note: 'Optional. Needed for automated version checks.',
|
||||
status:
|
||||
Boolean(systemStatus.proc_functions) &&
|
||||
Object.values(systemStatus.proc_functions).every(Boolean)
|
||||
? 'ok'
|
||||
: 'bad',
|
||||
pathColSpan: 3,
|
||||
},
|
||||
{
|
||||
id: 'storage',
|
||||
label: t('system.storage_writable'),
|
||||
path: 'storage/',
|
||||
min: '—',
|
||||
current: '—',
|
||||
status: systemStatus.storage_writable ? 'ok' : 'bad',
|
||||
},
|
||||
{
|
||||
id: 'updates',
|
||||
label: t('system.updates_writable'),
|
||||
path: 'storage/app/updates',
|
||||
min: '—',
|
||||
current: '—',
|
||||
status: systemStatus.updates_writable ? 'ok' : 'bad',
|
||||
},
|
||||
]
|
||||
}, [phpSelectedIsSufficient, systemStatus, t])
|
||||
|
||||
const visibleSystemChecks = useMemo(() => {
|
||||
const visibilityBySection = {
|
||||
insite: ['php', 'proc', 'storage', 'updates'],
|
||||
cli: ['php', 'composer', 'node', 'npm', 'proc', 'storage', 'updates'],
|
||||
ci: ['php', 'composer', 'node', 'npm', 'tar', 'rsync', 'proc', 'storage', 'updates'],
|
||||
info: [],
|
||||
}
|
||||
const allowed = new Set(visibilityBySection[systemSection] || [])
|
||||
return systemChecks.filter((check) => allowed.has(check.id))
|
||||
}, [systemChecks, systemSection])
|
||||
|
||||
|
||||
const handleLogoUpload = async (file, settingKey) => {
|
||||
if (!file) return
|
||||
setGeneralUploading(true)
|
||||
@@ -822,6 +1036,74 @@ function Acp({ isAdmin }) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderSystemRequirementsPanel() {
|
||||
return (
|
||||
<div className="bb-acp-panel">
|
||||
<div className="bb-acp-panel-header">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<h5 className="mb-0">{t('system.requirements')}</h5>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
{t('acp.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
{!systemStatus && <p className="bb-muted mb-0">{t('system.not_found')}</p>}
|
||||
{systemStatus && (
|
||||
<table className="bb-acp-stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('system.check')}</th>
|
||||
<th>{t('system.path')}</th>
|
||||
<th>{t('system.min_version')}</th>
|
||||
<th>{t('system.current_version')}</th>
|
||||
<th>{t('system.status')}</th>
|
||||
<th>{t('system.recheck')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visibleSystemChecks.map((check) => (
|
||||
<tr key={check.id}>
|
||||
<td>{check.label}</td>
|
||||
<td className="bb-acp-stats-value text-start" colSpan={check.pathColSpan || 1}>
|
||||
{check.path}
|
||||
{check.note && <div className="bb-muted mt-1 text-center">{check.note}</div>}
|
||||
</td>
|
||||
{!check.pathColSpan && (
|
||||
<>
|
||||
<td className="bb-acp-stats-value">{check.min}</td>
|
||||
<td className="bb-acp-stats-value">{check.current}</td>
|
||||
</>
|
||||
)}
|
||||
<td className="bb-acp-stats-value">
|
||||
<StatusIcon status={check.status} />
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
{t('system.recheck')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdmin) {
|
||||
handleVersionCheck()
|
||||
@@ -2516,7 +2798,7 @@ function Acp({ isAdmin }) {
|
||||
<Tabs defaultActiveKey="general" className="mb-3">
|
||||
<Tab eventKey="general" title={t('acp.general')}>
|
||||
<Row className="g-4">
|
||||
<Col lg={3} xl={2}>
|
||||
<Col xs={12} lg="auto">
|
||||
<div className="bb-acp-sidebar">
|
||||
<div className="bb-acp-sidebar-section">
|
||||
<div className="bb-acp-sidebar-title">{t('acp.quick_access')}</div>
|
||||
@@ -2576,13 +2858,7 @@ function Acp({ isAdmin }) {
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col lg={9} xl={10}>
|
||||
<div className="bb-acp-panel mb-4">
|
||||
<div className="bb-acp-panel-header">
|
||||
<h5 className="mb-1">{t('acp.welcome_title')}</h5>
|
||||
<p className="bb-muted mb-0">{t('acp.general_hint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Col xs={12} lg>
|
||||
<div className="bb-acp-panel mb-4">
|
||||
<div className="bb-acp-panel-header">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
@@ -3507,255 +3783,186 @@ function Acp({ isAdmin }) {
|
||||
<Tab eventKey="system" title={t('acp.system')}>
|
||||
{systemError && <p className="text-danger">{systemError}</p>}
|
||||
{systemLoading && <p className="bb-muted">{t('acp.loading')}</p>}
|
||||
{!systemLoading && systemStatus && (
|
||||
{!systemLoading && (
|
||||
<Row className="g-4">
|
||||
<Col xs={12} lg="auto">
|
||||
<div className="bb-acp-sidebar">
|
||||
<div className="bb-acp-sidebar-section">
|
||||
<div className="bb-acp-sidebar-title">{t('acp.system')}</div>
|
||||
<div className="list-group">
|
||||
<button
|
||||
type="button"
|
||||
className={`list-group-item list-group-item-action ${
|
||||
systemSection === 'info' ? 'is-active' : ''
|
||||
}`}
|
||||
onClick={() => setSystemSection('info')}
|
||||
>
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`list-group-item list-group-item-action ${
|
||||
systemSection === 'insite' ? 'is-active' : ''
|
||||
}`}
|
||||
onClick={() => setSystemSection('insite')}
|
||||
>
|
||||
Live Update
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`list-group-item list-group-item-action ${
|
||||
systemSection === 'cli' ? 'is-active' : ''
|
||||
}`}
|
||||
onClick={() => setSystemSection('cli')}
|
||||
>
|
||||
CLI
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`list-group-item list-group-item-action ${
|
||||
systemSection === 'ci' ? 'is-active' : ''
|
||||
}`}
|
||||
onClick={() => setSystemSection('ci')}
|
||||
>
|
||||
CI/CD
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={12} lg>
|
||||
{systemSection === 'info' && (
|
||||
<div className="bb-acp-panel">
|
||||
<div className="bb-acp-panel-header">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<h5 className="mb-0">{t('system.requirements')}</h5>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
{t('acp.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
<h5 className="mb-0">System overview</h5>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
<table className="bb-acp-stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('system.check')}</th>
|
||||
<th>{t('system.path')}</th>
|
||||
<th>{t('system.min_version')}</th>
|
||||
<th>{t('system.current_version')}</th>
|
||||
<th>{t('system.status')}</th>
|
||||
<th>{t('system.recheck')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>PHP</td>
|
||||
<td className="bb-acp-stats-value">{systemStatus.php_selected_path || '—'}</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.min_versions?.php || '—'}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.php_selected_version || '—'}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<StatusIcon
|
||||
status={systemStatus.php_selected_ok ? 'ok' : 'bad'}
|
||||
/>
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
{t('system.recheck')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Composer</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.composer || t('system.not_found')}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.min_versions?.composer || '—'}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.composer_version || '—'}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<StatusIcon status={systemStatus.composer ? 'ok' : 'bad'} />
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
{t('system.recheck')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Node</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.node || t('system.not_found')}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.min_versions?.node || '—'}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.node_version || '—'}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<StatusIcon status={systemStatus.node ? 'ok' : 'bad'} />
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
{t('system.recheck')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>npm</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.npm || t('system.not_found')}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.min_versions?.npm || '—'}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.npm_version || '—'}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<StatusIcon status={systemStatus.npm ? 'ok' : 'bad'} />
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
{t('system.recheck')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>tar</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.tar || t('system.not_found')}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">—</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.tar_version || '—'}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<StatusIcon status={systemStatus.tar ? 'ok' : 'bad'} />
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
{t('system.recheck')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>rsync</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.rsync || t('system.not_found')}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">—</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
{systemStatus.rsync_version || '—'}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<StatusIcon status={systemStatus.rsync ? 'ok' : 'bad'} />
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
{t('system.recheck')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>proc_* functions</td>
|
||||
<td className="bb-acp-stats-value" colSpan={3}>
|
||||
{systemStatus.proc_functions
|
||||
? Object.entries(systemStatus.proc_functions)
|
||||
.filter(([, ok]) => !ok)
|
||||
.map(([name]) => name)
|
||||
.join(', ')
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<StatusIcon
|
||||
status={
|
||||
Boolean(systemStatus.proc_functions) &&
|
||||
Object.values(systemStatus.proc_functions).every(Boolean)
|
||||
? 'ok'
|
||||
: 'bad'
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
{t('system.recheck')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t('system.storage_writable')}</td>
|
||||
<td className="bb-acp-stats-value">storage/</td>
|
||||
<td className="bb-acp-stats-value">—</td>
|
||||
<td className="bb-acp-stats-value">—</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<StatusIcon status={systemStatus.storage_writable ? 'ok' : 'bad'} />
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
{t('system.recheck')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t('system.updates_writable')}</td>
|
||||
<td className="bb-acp-stats-value">storage/app/updates</td>
|
||||
<td className="bb-acp-stats-value">—</td>
|
||||
<td className="bb-acp-stats-value">—</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<StatusIcon status={systemStatus.updates_writable ? 'ok' : 'bad'} />
|
||||
</td>
|
||||
<td className="bb-acp-stats-value">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
{t('system.recheck')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p className="bb-muted mb-0">
|
||||
Placeholder: summary, upgrade guidance, and environment health notes will
|
||||
live here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{systemSection === 'insite' && (
|
||||
<>
|
||||
<div className="bb-acp-panel mb-3">
|
||||
<div className="bb-acp-panel-body">
|
||||
<p className="bb-muted mb-0">Live update controls will appear here.</p>
|
||||
</div>
|
||||
</div>
|
||||
{renderSystemRequirementsPanel()}
|
||||
</>
|
||||
)}
|
||||
{systemSection === 'cli' && (
|
||||
<div className="bb-acp-panel">
|
||||
<div className="bb-acp-panel-header">
|
||||
<h5 className="mb-0">CLI</h5>
|
||||
<p className="bb-muted mb-0 mt-1">
|
||||
CLI default php: {systemStatus?.php_default || '—'} (
|
||||
{systemStatus?.php_default_version || 'unknown'}){' '}
|
||||
{cliDefaultPhpIsSufficient ? (
|
||||
<i className="bi bi-check-circle-fill text-success" aria-hidden="true" />
|
||||
) : (
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
overlay={
|
||||
<Tooltip id="cli-default-php-warning" data-bs-theme="light">
|
||||
You must select a custom PHP interpreter, as the system default is not sufficient.
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<i
|
||||
className="bi bi-exclamation-triangle-fill text-warning"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
{systemCliError && <p className="text-danger">{systemCliError}</p>}
|
||||
<Form onSubmit={handleSystemCliSave}>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>PHP interpreter</Form.Label>
|
||||
<Form.Select
|
||||
className="mb-2"
|
||||
value={systemCliSettings.php_mode}
|
||||
onChange={(event) =>
|
||||
setSystemCliSettings((prev) => ({
|
||||
...prev,
|
||||
php_mode: event.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="php">php (system default)</option>
|
||||
<option value="custom">Custom binary (e.g. keyhelp-php84)</option>
|
||||
</Form.Select>
|
||||
{systemCliSettings.php_mode === 'custom' && (
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="keyhelp-php84"
|
||||
value={systemCliSettings.php_custom}
|
||||
onChange={(event) =>
|
||||
setSystemCliSettings((prev) => ({
|
||||
...prev,
|
||||
php_custom: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Form.Text className="bb-muted">
|
||||
Minimum required PHP (from composer.json):{' '}
|
||||
{systemStatus?.min_versions?.php || 'unknown'}. Use a custom binary
|
||||
on like php84. On KeyHelp setups use e.g. `keyhelp-php84`.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Button type="submit" variant="dark" disabled={systemCliSaving}>
|
||||
{t('acp.save')}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{systemSection === 'cli' && renderSystemRequirementsPanel()}
|
||||
{systemSection === 'ci' && (
|
||||
<>
|
||||
<div className="bb-acp-panel">
|
||||
<div className="bb-acp-panel-header">
|
||||
<h5 className="mb-0">CI/CD</h5>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
<p className="bb-muted mb-0">
|
||||
Placeholder: CI/CD pipelines, runner requirements, and deployment logs will
|
||||
live here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{renderSystemRequirementsPanel()}
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab eventKey="custom" title="Custom">
|
||||
<Row className="g-4">
|
||||
<Col xs={12}>
|
||||
<div className="bb-acp-panel">
|
||||
<div className="bb-acp-panel-header">
|
||||
<h5 className="mb-0">Custom</h5>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
<p className="bb-muted mb-0">
|
||||
Place site-specific assets or overrides in `/custom` and `public/custom`.
|
||||
These paths are preserved during in-place updates.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<Modal show={showModal} onHide={handleReset} centered size="lg">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, Card, Container, Form } from 'react-bootstrap'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -53,10 +53,18 @@ export default function Login() {
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
required
|
||||
/>
|
||||
<div className="mt-2 text-end">
|
||||
<Link to="/reset-password">{t('auth.forgot_password')}</Link>
|
||||
</div>
|
||||
</Form.Group>
|
||||
<Button type="submit" variant="dark" disabled={loading}>
|
||||
<div className="d-flex w-100 align-items-center gap-2">
|
||||
<Button as={Link} to="/" type="button" variant="outline-secondary" disabled={loading}>
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" className="ms-auto bb-accent-button" disabled={loading}>
|
||||
{loading ? t('form.signing_in') : t('form.sign_in')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
113
resources/js/pages/ResetPassword.jsx
Normal file
113
resources/js/pages/ResetPassword.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, Card, Container, Form } from 'react-bootstrap'
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { requestPasswordReset, resetPassword } from '../api/client'
|
||||
|
||||
export default function ResetPassword() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const token = searchParams.get('token') || ''
|
||||
const emailFromLink = searchParams.get('email') || ''
|
||||
const isResetFlow = token.length > 0
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (emailFromLink) {
|
||||
setEmail(emailFromLink)
|
||||
}
|
||||
}, [emailFromLink])
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
try {
|
||||
if (isResetFlow) {
|
||||
await resetPassword({
|
||||
token,
|
||||
email,
|
||||
password,
|
||||
password_confirmation: passwordConfirmation,
|
||||
})
|
||||
navigate('/login')
|
||||
} else {
|
||||
await requestPasswordReset(email)
|
||||
navigate('/')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container fluid className="py-5">
|
||||
<Card className="bb-card mx-auto" style={{ maxWidth: '480px' }}>
|
||||
<Card.Body>
|
||||
<Card.Title className="mb-3">
|
||||
{isResetFlow ? t('auth.reset_password_title') : t('auth.forgot_password')}
|
||||
</Card.Title>
|
||||
<Card.Text className="bb-muted">
|
||||
{isResetFlow ? t('auth.reset_password_hint') : t('auth.forgot_password_hint')}
|
||||
</Card.Text>
|
||||
{error && <p className="text-danger">{error}</p>}
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className="mb-4">
|
||||
<Form.Label>{t('form.email')}</Form.Label>
|
||||
<Form.Control
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
placeholder={t('auth.reset_email_placeholder')}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
{isResetFlow && (
|
||||
<>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t('form.password')}</Form.Label>
|
||||
<Form.Control
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-4">
|
||||
<Form.Label>{t('auth.confirm_password')}</Form.Label>
|
||||
<Form.Control
|
||||
type="password"
|
||||
value={passwordConfirmation}
|
||||
onChange={(event) => setPasswordConfirmation(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
</>
|
||||
)}
|
||||
<div className="d-flex w-100 align-items-center gap-2">
|
||||
<Button as={Link} to="/login" type="button" variant="outline-secondary" disabled={loading}>
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" className="ms-auto bb-accent-button" disabled={loading}>
|
||||
{loading
|
||||
? isResetFlow
|
||||
? t('auth.resetting_password')
|
||||
: t('auth.sending_reset_link')
|
||||
: isResetFlow
|
||||
? t('auth.reset_password_submit')
|
||||
: t('auth.send_reset_link')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -81,6 +81,18 @@
|
||||
"auth.login_title": "Anmelden",
|
||||
"auth.login_identifier": "E-Mail oder Benutzername",
|
||||
"auth.login_placeholder": "name@example.com oder benutzername",
|
||||
"auth.forgot_password": "Passwort vergessen?",
|
||||
"auth.forgot_password_hint": "Gib die E-Mail-Adresse deines Kontos ein, dann senden wir dir einen Link zum Zuruecksetzen.",
|
||||
"auth.reset_email_placeholder": "name@example.com",
|
||||
"auth.send_reset_link": "Reset-Link senden",
|
||||
"auth.sending_reset_link": "Wird gesendet...",
|
||||
"auth.reset_link_sent": "Falls ein Konto existiert, wurde ein Passwort-Reset-Link gesendet.",
|
||||
"auth.reset_password_title": "Passwort zuruecksetzen",
|
||||
"auth.reset_password_hint": "Gib deine E-Mail-Adresse ein und waehle ein neues Passwort.",
|
||||
"auth.reset_password_submit": "Passwort zuruecksetzen",
|
||||
"auth.resetting_password": "Wird zurueckgesetzt...",
|
||||
"auth.password_reset_success": "Passwort erfolgreich zurueckgesetzt. Du kannst dich jetzt anmelden.",
|
||||
"auth.confirm_password": "Passwort bestaetigen",
|
||||
"auth.register_hint": "Registriere dich mit E-Mail und einem eindeutigen Benutzernamen.",
|
||||
"auth.verify_notice": "Bitte bestätige deine E-Mail-Adresse, bevor du dich anmeldest.",
|
||||
"auth.register_title": "Konto erstellen",
|
||||
@@ -174,6 +186,9 @@
|
||||
"version.up_to_date": "Aktuell",
|
||||
"version.update_available": "Update verfügbar (v{{version}})",
|
||||
"version.update_available_short": "Update verfügbar",
|
||||
"version.refresh_prompt_title": "Update verfuegbar",
|
||||
"version.refresh_prompt_body": "Ein neuerer Build ({{build}}) ist verfuegbar. Jetzt neu laden, um die aktuelle Version zu verwenden?",
|
||||
"version.remind_later": "Spaeter",
|
||||
"version.unknown": "Version unbekannt",
|
||||
"version.update_now": "Jetzt aktualisieren",
|
||||
"version.update_title": "System aktualisieren",
|
||||
|
||||
@@ -81,6 +81,18 @@
|
||||
"auth.login_title": "Log in",
|
||||
"auth.login_identifier": "Email or username",
|
||||
"auth.login_placeholder": "name@example.com or username",
|
||||
"auth.forgot_password": "Forgot password?",
|
||||
"auth.forgot_password_hint": "Enter your account email and we will send you a password reset link.",
|
||||
"auth.reset_email_placeholder": "name@example.com",
|
||||
"auth.send_reset_link": "Send reset link",
|
||||
"auth.sending_reset_link": "Sending...",
|
||||
"auth.reset_link_sent": "If an account exists, a password reset link has been sent.",
|
||||
"auth.reset_password_title": "Reset password",
|
||||
"auth.reset_password_hint": "Enter your email and choose a new password.",
|
||||
"auth.reset_password_submit": "Reset password",
|
||||
"auth.resetting_password": "Resetting...",
|
||||
"auth.password_reset_success": "Password reset successful. You can now sign in.",
|
||||
"auth.confirm_password": "Confirm password",
|
||||
"auth.register_hint": "Register with an email and a unique username.",
|
||||
"auth.verify_notice": "Check your email to verify your account before logging in.",
|
||||
"auth.register_title": "Create account",
|
||||
@@ -164,6 +176,9 @@
|
||||
"version.up_to_date": "Up to date",
|
||||
"version.update_available": "Update available (v{{version}})",
|
||||
"version.update_available_short": "Update available",
|
||||
"version.refresh_prompt_title": "Update available",
|
||||
"version.refresh_prompt_body": "A newer build ({{build}}) is available. Refresh now to load the latest version?",
|
||||
"version.remind_later": "Later",
|
||||
"version.unknown": "Version unknown",
|
||||
"version.update_now": "Update now",
|
||||
"version.update_title": "Update system",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\InstallerController;
|
||||
use App\Http\Controllers\PingController;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
@@ -29,6 +30,8 @@ Route::get('/reset-password', function () {
|
||||
return view('app');
|
||||
})->name('password.reset');
|
||||
|
||||
Route::get('/ping', PingController::class);
|
||||
|
||||
Route::get('/{any}', function () {
|
||||
if (!file_exists(base_path('.env'))) {
|
||||
return redirect('/install');
|
||||
|
||||
8
scripts/hooks/pre-commit
Normal file
8
scripts/hooks/pre-commit
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Keep commits possible when local DB is offline.
|
||||
if ! php artisan version:fetch >/dev/null 2>&1; then
|
||||
echo "pre-commit: skipped 'php artisan version:fetch' (database unreachable)." >&2
|
||||
echo "pre-commit: start MySQL and run it manually when needed." >&2
|
||||
fi
|
||||
18
tests/Feature/PingControllerTest.php
Normal file
18
tests/Feature/PingControllerTest.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Setting;
|
||||
|
||||
it('returns ping status with build and notification state', function (): void {
|
||||
Setting::updateOrCreate(['key' => 'build'], ['value' => '1337']);
|
||||
|
||||
$response = $this->getJson('/ping');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'connect' => 'ok',
|
||||
'version_status' => [
|
||||
'build' => 1337,
|
||||
],
|
||||
'notification_state' => false,
|
||||
]);
|
||||
});
|
||||
@@ -398,25 +398,20 @@ it('handles migration failure', function (): void {
|
||||
File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']);
|
||||
File::shouldReceive('copyDirectory')->andReturnTrue();
|
||||
|
||||
$artisanPath = base_path('artisan');
|
||||
$originalArtisan = file_get_contents($artisanPath);
|
||||
file_put_contents($artisanPath, "#!/usr/bin/env php\n<?php exit(1);\n");
|
||||
chmod($artisanPath, 0755);
|
||||
putenv('SYSTEM_UPDATE_PHP_BINARY=/nope');
|
||||
$_ENV['SYSTEM_UPDATE_PHP_BINARY'] = '/nope';
|
||||
$_SERVER['SYSTEM_UPDATE_PHP_BINARY'] = '/nope';
|
||||
|
||||
withFakeBin([
|
||||
'tar' => "#!/bin/sh\nexit 0\n",
|
||||
'composer' => "#!/bin/sh\nexit 0\n",
|
||||
'npm' => "#!/bin/sh\nexit 0\n",
|
||||
], function () use ($artisanPath, $originalArtisan): void {
|
||||
try {
|
||||
], function (): void {
|
||||
Sanctum::actingAs(makeAdminForSystemUpdate());
|
||||
$response = $this->postJson('/api/system/update');
|
||||
|
||||
$response->assertStatus(500);
|
||||
$response->assertJsonFragment(['message' => 'Migrations failed.']);
|
||||
} finally {
|
||||
file_put_contents($artisanPath, $originalArtisan);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -424,6 +419,9 @@ it('handles fallback copyDirectory update success', function (): void {
|
||||
putenv('GITEA_OWNER=acme');
|
||||
putenv('GITEA_REPO=speedbb');
|
||||
putenv('GITEA_API_BASE=https://git.example.test/api/v1');
|
||||
putenv('SYSTEM_UPDATE_PHP_BINARY=php');
|
||||
$_ENV['SYSTEM_UPDATE_PHP_BINARY'] = 'php';
|
||||
$_SERVER['SYSTEM_UPDATE_PHP_BINARY'] = 'php';
|
||||
|
||||
Http::fake([
|
||||
'https://git.example.test/api/v1/repos/acme/speedbb/releases/latest' => Http::response([
|
||||
@@ -438,25 +436,21 @@ it('handles fallback copyDirectory update success', function (): void {
|
||||
File::shouldReceive('directories')->andReturn(['/tmp/extract/speedbb']);
|
||||
File::shouldReceive('copyDirectory')->andReturnTrue();
|
||||
|
||||
$artisanPath = base_path('artisan');
|
||||
$originalArtisan = file_get_contents($artisanPath);
|
||||
file_put_contents($artisanPath, "#!/usr/bin/env php\n<?php exit(0);\n");
|
||||
chmod($artisanPath, 0755);
|
||||
putenv('SYSTEM_UPDATE_PHP_BINARY=php');
|
||||
$_ENV['SYSTEM_UPDATE_PHP_BINARY'] = 'php';
|
||||
$_SERVER['SYSTEM_UPDATE_PHP_BINARY'] = 'php';
|
||||
|
||||
withFakeBin([
|
||||
'tar' => "#!/bin/sh\nexit 0\n",
|
||||
'composer' => "#!/bin/sh\nexit 0\n",
|
||||
'npm' => "#!/bin/sh\nexit 0\n",
|
||||
], function () use ($artisanPath, $originalArtisan): void {
|
||||
try {
|
||||
'php' => "#!/bin/sh\nexit 0\n",
|
||||
], function (): void {
|
||||
Sanctum::actingAs(makeAdminForSystemUpdate());
|
||||
$response = $this->postJson('/api/system/update');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonFragment(['message' => 'Update finished.']);
|
||||
$response->assertJsonStructure(['used_rsync']);
|
||||
} finally {
|
||||
file_put_contents($artisanPath, $originalArtisan);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -285,3 +285,28 @@ it('updates user name and email as admin', function (): void {
|
||||
expect($target->email)->toBe('new@example.com');
|
||||
expect($target->email_verified_at)->toBeNull();
|
||||
});
|
||||
|
||||
it('marks email verified when assigning founder role', function (): void {
|
||||
$admin = makeAdmin();
|
||||
$founderRole = Role::firstOrCreate(['name' => 'ROLE_FOUNDER'], ['color' => '#111111']);
|
||||
$target = User::factory()->create([
|
||||
'name' => 'Target',
|
||||
'email' => 'target@example.com',
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
|
||||
$admin->roles()->syncWithoutDetaching([$founderRole->id]);
|
||||
|
||||
Sanctum::actingAs($admin);
|
||||
$response = $this->patchJson("/api/users/{$target->id}", [
|
||||
'name' => 'Target',
|
||||
'email' => 'target@example.com',
|
||||
'rank_id' => null,
|
||||
'roles' => ['ROLE_FOUNDER'],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$target->refresh();
|
||||
expect($target->email_verified_at)->not()->toBeNull();
|
||||
});
|
||||
|
||||
@@ -1,94 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace s9e\TextFormatter {
|
||||
class Parser
|
||||
{
|
||||
public function parse(string $text): string
|
||||
{
|
||||
return '<r/>';
|
||||
}
|
||||
}
|
||||
use App\Actions\BbcodeFormatter;
|
||||
|
||||
class Renderer
|
||||
{
|
||||
public function render(string $xml): string
|
||||
{
|
||||
return '<p>ok</p>';
|
||||
}
|
||||
}
|
||||
|
||||
class Configurator
|
||||
{
|
||||
public static bool $returnEmpty = false;
|
||||
public object $plugins;
|
||||
public object $tags;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->plugins = new class {
|
||||
public function load(string $name): object
|
||||
{
|
||||
return new class {
|
||||
public function addFromRepository(string $name): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$this->tags = new class implements \ArrayAccess {
|
||||
public array $store = [];
|
||||
public function add($name)
|
||||
{
|
||||
$obj = new \stdClass();
|
||||
$this->store[$name] = $obj;
|
||||
return $obj;
|
||||
}
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return array_key_exists($offset, $this->store);
|
||||
}
|
||||
public function offsetGet($offset): mixed
|
||||
{
|
||||
return $this->store[$offset] ?? null;
|
||||
}
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
$this->store[$offset] = $value;
|
||||
}
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->store[$offset]);
|
||||
}
|
||||
};
|
||||
|
||||
$this->tags['QUOTE'] = new \stdClass();
|
||||
}
|
||||
|
||||
public function finalize(): array
|
||||
{
|
||||
if (self::$returnEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'parser' => new Parser(),
|
||||
'renderer' => new Renderer(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
use App\Actions\BbcodeFormatter;
|
||||
|
||||
it('returns empty string for null and empty input', function (): void {
|
||||
it('returns empty string for null and empty input', function (): void {
|
||||
expect(BbcodeFormatter::format(null))->toBe('');
|
||||
expect(BbcodeFormatter::format(''))->toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
it('formats bbcode content', function (): void {
|
||||
it('formats bbcode content', function (): void {
|
||||
$parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser');
|
||||
$parserProp->setAccessible(true);
|
||||
$parserProp->setValue(
|
||||
@@ -110,9 +29,9 @@ namespace {
|
||||
$html = BbcodeFormatter::format('[b]Bold[/b]');
|
||||
|
||||
expect($html)->toContain('<b>');
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes parser and renderer when not set', function (): void {
|
||||
it('initializes parser and renderer when not set', function (): void {
|
||||
$parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser');
|
||||
$parserProp->setAccessible(true);
|
||||
$parserProp->setValue(null);
|
||||
@@ -126,10 +45,45 @@ namespace {
|
||||
expect($html)->toBeString();
|
||||
expect($parserProp->getValue())->not->toBeNull();
|
||||
expect($rendererProp->getValue())->not->toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when bbcode formatter cannot initialize', function (): void {
|
||||
\s9e\TextFormatter\Configurator::$returnEmpty = true;
|
||||
it('build returns parser and renderer', function (): void {
|
||||
putenv('BBCODE_FORCE_FAIL');
|
||||
unset($_ENV['BBCODE_FORCE_FAIL'], $_SERVER['BBCODE_FORCE_FAIL']);
|
||||
|
||||
$ref = new ReflectionMethod(BbcodeFormatter::class, 'build');
|
||||
$ref->setAccessible(true);
|
||||
|
||||
$result = $ref->invoke(null);
|
||||
|
||||
expect($result)->toBeArray();
|
||||
expect($result)->toHaveCount(2);
|
||||
expect($result[0])->toBeInstanceOf(\s9e\TextFormatter\Parser::class);
|
||||
expect($result[1])->toBeInstanceOf(\s9e\TextFormatter\Renderer::class);
|
||||
});
|
||||
|
||||
it('formats with real build when parser is reset', function (): void {
|
||||
putenv('BBCODE_FORCE_FAIL');
|
||||
unset($_ENV['BBCODE_FORCE_FAIL'], $_SERVER['BBCODE_FORCE_FAIL']);
|
||||
|
||||
$parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser');
|
||||
$parserProp->setAccessible(true);
|
||||
$parserProp->setValue(null);
|
||||
|
||||
$rendererProp = new ReflectionProperty(BbcodeFormatter::class, 'renderer');
|
||||
$rendererProp->setAccessible(true);
|
||||
$rendererProp->setValue(null);
|
||||
|
||||
$html = BbcodeFormatter::format('[b]Bold[/b]');
|
||||
expect($html)->toBeString();
|
||||
expect($parserProp->getValue())->not->toBeNull();
|
||||
expect($rendererProp->getValue())->not->toBeNull();
|
||||
});
|
||||
|
||||
it('throws when bbcode formatter cannot initialize', function (): void {
|
||||
putenv('BBCODE_FORCE_FAIL=1');
|
||||
$_ENV['BBCODE_FORCE_FAIL'] = '1';
|
||||
$_SERVER['BBCODE_FORCE_FAIL'] = '1';
|
||||
|
||||
$parserProp = new ReflectionProperty(BbcodeFormatter::class, 'parser');
|
||||
$parserProp->setAccessible(true);
|
||||
@@ -145,11 +99,11 @@ namespace {
|
||||
} catch (Throwable $e) {
|
||||
expect($e)->toBeInstanceOf(RuntimeException::class);
|
||||
} finally {
|
||||
\s9e\TextFormatter\Configurator::$returnEmpty = false;
|
||||
putenv('BBCODE_FORCE_FAIL');
|
||||
unset($_ENV['BBCODE_FORCE_FAIL'], $_SERVER['BBCODE_FORCE_FAIL']);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
afterEach(function (): void {
|
||||
\Mockery::close();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,10 +20,10 @@ it('version set fails when invalid version', function (): void {
|
||||
expect($exitCode)->toBe(1);
|
||||
});
|
||||
|
||||
it('version fetch fails when no version', function (): void {
|
||||
it('version fetch succeeds without db version when composer version exists', function (): void {
|
||||
Setting::where('key', 'version')->delete();
|
||||
$exitCode = \Illuminate\Support\Facades\Artisan::call('version:fetch');
|
||||
expect($exitCode)->toBe(1);
|
||||
expect($exitCode)->toBe(0);
|
||||
});
|
||||
|
||||
it('version release fails when missing config', function (): void {
|
||||
|
||||
@@ -73,6 +73,7 @@ it('returns system status for admins', function (): void {
|
||||
expect($payload)->toHaveKeys([
|
||||
'php',
|
||||
'php_default',
|
||||
'php_default_version',
|
||||
'composer',
|
||||
'composer_version',
|
||||
'node',
|
||||
|
||||
@@ -11,17 +11,6 @@ namespace App\Console\Commands {
|
||||
return \file_get_contents($path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists(__NAMESPACE__ . '\\json_encode')) {
|
||||
function json_encode($value, int $flags = 0): string|false
|
||||
{
|
||||
if (!empty($GLOBALS['version_fetch_json_encode_false']) && is_array($value) && array_key_exists('build', $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \json_encode($value, $flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -40,84 +29,47 @@ namespace {
|
||||
file_put_contents($path, $original);
|
||||
}
|
||||
$GLOBALS['version_fetch_file_get_contents_false'] = false;
|
||||
$GLOBALS['version_fetch_json_encode_false'] = false;
|
||||
$originalPath = $GLOBALS['version_fetch_path'] ?? null;
|
||||
if ($originalPath !== null) {
|
||||
putenv("PATH={$originalPath}");
|
||||
$_ENV['PATH'] = $originalPath;
|
||||
$_SERVER['PATH'] = $originalPath;
|
||||
unset($GLOBALS['version_fetch_path']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('fetches build count and syncs composer metadata', function (): void {
|
||||
it('syncs version and build from composer metadata', function (): void {
|
||||
withComposerBackupForFetch(function (): void {
|
||||
Setting::updateOrCreate(['key' => 'version'], ['value' => '1.2.3']);
|
||||
Setting::updateOrCreate(['key' => 'version'], ['value' => '0.0.0']);
|
||||
Setting::updateOrCreate(['key' => 'build'], ['value' => '0']);
|
||||
|
||||
$composer = json_decode((string) file_get_contents(base_path('composer.json')), true);
|
||||
$expectedVersion = (string) ($composer['version'] ?? '');
|
||||
$expectedBuild = (string) ($composer['build'] ?? '');
|
||||
|
||||
$exitCode = Artisan::call('version:fetch');
|
||||
expect($exitCode)->toBe(0);
|
||||
|
||||
$build = Setting::where('key', 'build')->value('value');
|
||||
expect(is_numeric($build))->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when build count cannot be resolved', function (): void {
|
||||
withComposerBackupForFetch(function (): void {
|
||||
$GLOBALS['version_fetch_path'] = getenv('PATH') ?: '';
|
||||
putenv('PATH=/nope');
|
||||
$_ENV['PATH'] = '/nope';
|
||||
$_SERVER['PATH'] = '/nope';
|
||||
|
||||
Setting::updateOrCreate(['key' => 'version'], ['value' => '1.2.3']);
|
||||
|
||||
$exitCode = Artisan::call('version:fetch');
|
||||
expect($exitCode)->toBe(1);
|
||||
expect(Setting::where('key', 'version')->value('value'))->toBe($expectedVersion);
|
||||
expect(Setting::where('key', 'build')->value('value'))->toBe($expectedBuild);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when composer.json cannot be decoded', function (): void {
|
||||
withComposerBackupForFetch(function (string $path): void {
|
||||
file_put_contents($path, 'not-json');
|
||||
|
||||
Setting::updateOrCreate(['key' => 'version'], ['value' => '1.2.3']);
|
||||
|
||||
$exitCode = Artisan::call('version:fetch');
|
||||
expect($exitCode)->toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when composer.json is not readable', function (): void {
|
||||
it('fails when composer.json is missing build', function (): void {
|
||||
withComposerBackupForFetch(function (string $path): void {
|
||||
chmod($path, 0000);
|
||||
|
||||
Setting::updateOrCreate(['key' => 'version'], ['value' => '1.2.3']);
|
||||
$data = json_decode((string) file_get_contents($path), true);
|
||||
unset($data['build']);
|
||||
file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||
|
||||
$exitCode = Artisan::call('version:fetch');
|
||||
expect($exitCode)->toBe(1);
|
||||
|
||||
chmod($path, 0644);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when file_get_contents returns false', function (): void {
|
||||
withComposerBackupForFetch(function (): void {
|
||||
$GLOBALS['version_fetch_file_get_contents_false'] = true;
|
||||
|
||||
Setting::updateOrCreate(['key' => 'version'], ['value' => '1.2.3']);
|
||||
|
||||
$exitCode = Artisan::call('version:fetch');
|
||||
expect($exitCode)->toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when json_encode returns false', function (): void {
|
||||
withComposerBackupForFetch(function (): void {
|
||||
$GLOBALS['version_fetch_json_encode_false'] = true;
|
||||
|
||||
Setting::updateOrCreate(['key' => 'version'], ['value' => '1.2.3']);
|
||||
|
||||
$exitCode = Artisan::call('version:fetch');
|
||||
expect($exitCode)->toBe(1);
|
||||
});
|
||||
|
||||
12
tests/run-shell-tests.sh
Normal file
12
tests/run-shell-tests.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if ! command -v bats >/dev/null 2>&1; then
|
||||
echo "bats is not installed. Install with: brew install bats-core" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bats tests/shell/git_update.bats
|
||||
52
tests/shell/git_update.bats
Normal file
52
tests/shell/git_update.bats
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
||||
SCRIPT_PATH="$REPO_ROOT/git_update.sh"
|
||||
TMP_ROOT="$BATS_TEST_TMPDIR/work"
|
||||
mkdir -p "$TMP_ROOT/bin"
|
||||
}
|
||||
|
||||
@test "git_update.sh can be sourced without running main" {
|
||||
run bash -lc "source '$SCRIPT_PATH' >/dev/null 2>&1; echo sourced"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == "sourced" ]]
|
||||
}
|
||||
|
||||
@test "resolve_configured_php_bin accepts command name from PATH" {
|
||||
cat >"$TMP_ROOT/bin/php84" <<'SH'
|
||||
#!/usr/bin/env sh
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$TMP_ROOT/bin/php84"
|
||||
|
||||
run bash -lc "PATH='$TMP_ROOT/bin':\$PATH; source '$SCRIPT_PATH'; resolve_configured_php_bin 'php84' 'php'"
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "php84" ]
|
||||
}
|
||||
|
||||
@test "resolve_configured_php_bin rejects unknown custom command" {
|
||||
run bash -lc "PATH='$TMP_ROOT/bin'; source '$SCRIPT_PATH'; resolve_configured_php_bin 'does-not-exist' 'php'"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"Configured PHP binary 'does-not-exist' is not executable/resolvable."* ]]
|
||||
}
|
||||
|
||||
@test "enforce_php_requirement passes for satisfied constraint" {
|
||||
cat >"$TMP_ROOT/composer.json" <<'JSON'
|
||||
{"require":{"php":">=8.0"}}
|
||||
JSON
|
||||
|
||||
run bash -lc "cd '$TMP_ROOT'; source '$SCRIPT_PATH'; enforce_php_requirement php"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"PHP requirement check passed"* ]]
|
||||
}
|
||||
|
||||
@test "enforce_php_requirement fails for unsatisfied constraint" {
|
||||
cat >"$TMP_ROOT/composer.json" <<'JSON'
|
||||
{"require":{"php":">=99.0"}}
|
||||
JSON
|
||||
|
||||
run bash -lc "cd '$TMP_ROOT'; source '$SCRIPT_PATH'; enforce_php_requirement php"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"PHP requirement check failed"* ]]
|
||||
}
|
||||
Reference in New Issue
Block a user