mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-09-03 13:51:08 +00:00
feat(operations): fresh platform-operations implementation
Full rewrite of the installer (incl. the finish step), self-update pipeline, settings store, cron webhook, dashboard/bootstrap surface, environment manager, wizard login, config endpoint and shared helpers. Byte-compatible public interfaces and observable behavior, including documented legacy quirks; verified by the pilot behavioral suite and the full test suite.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Platform\Operations\Installation\Http\Controllers;
|
||||
|
||||
use App\Platform\Http\Controller;
|
||||
use App\Platform\Operations\Installation\Application\EnvironmentManager;
|
||||
use App\Platform\Operations\Installation\Http\Requests\DomainEnvironmentRequest;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
/**
|
||||
* The wizard's domain step: records the host this instance will be served
|
||||
* from, so sessions and stateful API calls are scoped to it.
|
||||
*/
|
||||
class AppDomainController extends Controller
|
||||
{
|
||||
/**
|
||||
* Compiled configuration is dropped first, otherwise the manager would
|
||||
* compare the submitted domain against a cached application URL.
|
||||
*
|
||||
* The step answers success unconditionally — a failed write has never been
|
||||
* reported to the client here, and the wizard's later steps rewrite the
|
||||
* same two variables anyway. Surfacing an error at this point would change
|
||||
* the contract the wizard front end is built against.
|
||||
*/
|
||||
public function __invoke(DomainEnvironmentRequest $request)
|
||||
{
|
||||
Artisan::call('optimize:clear');
|
||||
|
||||
(new EnvironmentManager)->saveDomainVariables($request);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Platform\Operations\Installation\Http\Controllers;
|
||||
|
||||
use App\Platform\Http\Controller;
|
||||
use App\Platform\Operations\Installation\Application\EnvironmentManager;
|
||||
use App\Platform\Operations\Installation\Application\InstallationState;
|
||||
use App\Platform\Operations\Installation\Http\Requests\DatabaseEnvironmentRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
/**
|
||||
* The wizard's database step, in both directions: what the form should be
|
||||
* prefilled with, and what happens once it comes back.
|
||||
*/
|
||||
class DatabaseConfigurationController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var EnvironmentManager
|
||||
*/
|
||||
protected $EnvironmentManager;
|
||||
|
||||
public function __construct(EnvironmentManager $environmentManager)
|
||||
{
|
||||
$this->EnvironmentManager = $environmentManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches go first so the manager works against the environment as it sits
|
||||
* on disk. A clean write is followed by building the installation out in
|
||||
* place: public storage symlink, the whole migration chain with seeders,
|
||||
* and the version stamp.
|
||||
*
|
||||
* The application key is deliberately not regenerated here. Rotating it
|
||||
* mid-wizard invalidates the session the browser is holding, which surfaces
|
||||
* as a token mismatch on the very next step; an instance that needs a fresh
|
||||
* key generates it before the wizard runs.
|
||||
*/
|
||||
public function saveDatabaseEnvironment(DatabaseEnvironmentRequest $request)
|
||||
{
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('cache:clear');
|
||||
|
||||
$results = $this->EnvironmentManager->saveDatabaseVariables($request);
|
||||
|
||||
if (array_key_exists('success', $results)) {
|
||||
Artisan::call('optimize:clear');
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('cache:clear');
|
||||
Artisan::call('storage:link');
|
||||
Artisan::call('migrate --seed --force');
|
||||
|
||||
InstallationState::setCurrentVersion();
|
||||
}
|
||||
|
||||
return response()->json($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults for the database form. The driver comes from the query string,
|
||||
* falling back to whatever the runtime is configured with.
|
||||
*
|
||||
* A driver with no arm of its own is echoed back with the server defaults
|
||||
* rather than an empty config: the wizard chooses which form to render from
|
||||
* database_connection, so answering with nothing left the step blank and
|
||||
* the install stuck (as a DB_CONNECTION=mariadb compose file once did).
|
||||
*
|
||||
* The prefill key is database_host while the form posts back
|
||||
* database_hostname. The mismatch is what the front end expects.
|
||||
*/
|
||||
public function getDatabaseEnvironment(Request $request)
|
||||
{
|
||||
$connection = $request->connection ?? config('database.default');
|
||||
|
||||
$databaseData = match ($connection) {
|
||||
'sqlite' => [
|
||||
'database_connection' => 'sqlite',
|
||||
'database_name' => config('database.connections.sqlite.database') ?: 'storage/app/database.sqlite',
|
||||
],
|
||||
'pgsql' => [
|
||||
'database_connection' => 'pgsql',
|
||||
'database_host' => '127.0.0.1',
|
||||
'database_port' => 5432,
|
||||
],
|
||||
default => [
|
||||
'database_connection' => $connection,
|
||||
'database_host' => '127.0.0.1',
|
||||
'database_port' => 3306,
|
||||
],
|
||||
};
|
||||
|
||||
return response()->json([
|
||||
'config' => $databaseData,
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Platform\Operations\Installation\Http\Controllers;
|
||||
|
||||
use App\Platform\Http\Controller;
|
||||
use App\Platform\Operations\Installation\Application\FilePermissionChecker;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class FilePermissionsController extends Controller
|
||||
{
|
||||
protected FilePermissionChecker $permissions;
|
||||
|
||||
public function __construct(FilePermissionChecker $checker)
|
||||
{
|
||||
$this->permissions = $checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Second wizard gate: the writability of the folders listed in
|
||||
* config/installer.php.
|
||||
*/
|
||||
public function permissions(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'permissions' => $this->permissions->check(
|
||||
config('installer.permissions')
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Platform\Operations\Installation\Http\Controllers;
|
||||
|
||||
use App\Platform\Http\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* The wizard's closing step. Nothing is persisted here — completion state is
|
||||
* tracked through the wizard-step endpoint — so acknowledging is all there is
|
||||
* to do.
|
||||
*/
|
||||
class FinishController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Platform\Operations\Installation\Http\Controllers;
|
||||
|
||||
use App\Domains\Accounts\Models\User;
|
||||
use App\Platform\Http\Controller;
|
||||
use App\Platform\Operations\Installation\Authentication\InstallWizardAuth;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
/**
|
||||
* Signs the wizard in as the administrator the seeders just created, so the
|
||||
* remaining steps can call the authenticated API.
|
||||
*/
|
||||
class LoginController extends Controller
|
||||
{
|
||||
/**
|
||||
* There is exactly one candidate at this point in the install: the first
|
||||
* platform administrator, and the first company attached to them. Any
|
||||
* browser session that got this far is thrown away — the wizard carries on
|
||||
* with a bearer token limited to the wizard ability, and only one such
|
||||
* token is ever live at a time.
|
||||
*/
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$user = User::where('role', 'super admin')->first();
|
||||
|
||||
if ($user === null) {
|
||||
return response()->json([
|
||||
'message' => 'Super admin user not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$company = $user->companies()->first();
|
||||
|
||||
if ($company === null) {
|
||||
return response()->json([
|
||||
'message' => 'Super admin company not found.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
if ($request->hasSession()) {
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
}
|
||||
|
||||
$user->tokens()->where('name', InstallWizardAuth::TOKEN_NAME)->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'type' => 'Bearer',
|
||||
'token' => $user->createToken(
|
||||
InstallWizardAuth::TOKEN_NAME,
|
||||
[InstallWizardAuth::TOKEN_ABILITY],
|
||||
)->plainTextToken,
|
||||
'user' => $user,
|
||||
'company' => $company,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Platform\Operations\Installation\Http\Controllers;
|
||||
|
||||
use App\Platform\Http\Controller;
|
||||
use App\Platform\Operations\Installation\Application\InstallationState;
|
||||
use App\Platform\Operations\Models\Setting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Tracks how far through the setup wizard the operator has walked, so a
|
||||
* reloaded browser resumes on the step it left off.
|
||||
*/
|
||||
class OnboardingWizardController extends Controller
|
||||
{
|
||||
/**
|
||||
* Terminal value of the profile_complete setting.
|
||||
*/
|
||||
private const FINISHED = 'COMPLETED';
|
||||
|
||||
/**
|
||||
* Until the schema exists there is nowhere to read progress from, so the
|
||||
* very first step and the default language are answered from thin air.
|
||||
*/
|
||||
public function getStep(Request $request): JsonResponse
|
||||
{
|
||||
if (! InstallationState::isDbCreated()) {
|
||||
return response()->json([
|
||||
'profile_complete' => 0,
|
||||
'profile_language' => 'en',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'profile_complete' => Setting::getSetting('profile_complete'),
|
||||
'profile_language' => Setting::getSetting('profile_language'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record progress, unless the wizard has already run to completion — a
|
||||
* finished install must not be walked backwards into the setup flow.
|
||||
*/
|
||||
public function updateStep(Request $request): JsonResponse
|
||||
{
|
||||
$step = Setting::getSetting('profile_complete');
|
||||
|
||||
if ($step !== self::FINISHED) {
|
||||
Setting::setSetting('profile_complete', $request->input('profile_complete'));
|
||||
|
||||
$step = Setting::getSetting('profile_complete');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'profile_complete' => $step,
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveLanguage(Request $request): JsonResponse
|
||||
{
|
||||
Setting::setSetting('profile_language', $request->input('profile_language'));
|
||||
|
||||
return response()->json([
|
||||
'profile_language' => Setting::getSetting('profile_language'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Platform\Operations\Installation\Http\Controllers;
|
||||
|
||||
use App\Platform\Http\Controller;
|
||||
use App\Platform\Operations\Installation\Application\RequirementsChecker;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class RequirementsController extends Controller
|
||||
{
|
||||
protected RequirementsChecker $requirements;
|
||||
|
||||
public function __construct(RequirementsChecker $checker)
|
||||
{
|
||||
$this->requirements = $checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* First wizard gate: the interpreter version block plus the per-extension
|
||||
* verdicts drawn from config/installer.php.
|
||||
*/
|
||||
public function requirements(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'phpSupportInfo' => $this->requirements->checkPHPVersion(
|
||||
config('installer.core.minPhpVersion')
|
||||
),
|
||||
'requirements' => $this->requirements->check(
|
||||
config('installer.requirements')
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user