Setup Inertia (#593)

This commit is contained in:
Saeed Vaziry
2025-05-10 10:10:11 +02:00
committed by GitHub
parent 6eb88c7c6e
commit 38bafd7654
305 changed files with 13378 additions and 15435 deletions

View File

@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Post;
class AuthenticatedSessionController extends Controller
{
#[Get('login', name: 'login', middleware: 'guest')]
public function create(Request $request): Response
{
return Inertia::render('auth/login', [
'canResetPassword' => Route::has('password.request'),
'status' => $request->session()->get('status'),
]);
}
#[Post('login', name: 'login', middleware: 'guest')]
public function store(Request $request): RedirectResponse
{
$this->validate($request, [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
]);
$this->ensureIsNotRateLimited();
if (! Auth::attempt(['email' => $request->email, 'password' => $request->password], $request->remember)) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
Session::regenerate();
return redirect()->intended(route('servers', absolute: false));
}
#[Post('logout', name: 'logout', middleware: 'auth')]
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
protected function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout(request()));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => __('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
protected function throttleKey(): string
{
return Str::transliterate(Str::lower(request()->email).'|'.request()->ip());
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('confirm-password')]
#[Middleware('auth')]
class ConfirmablePasswordController extends Controller
{
#[Get('/', name: 'password.confirm')]
public function show(): Response
{
return Inertia::render('auth/confirm-password');
}
#[Post('/', name: 'password.confirm')]
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix(('reset-password'))]
#[Middleware('guest')]
class NewPasswordController extends Controller
{
#[Get('{token}', name: 'password.reset')]
public function create(Request $request): Response
{
return Inertia::render('auth/reset-password', [
'email' => $request->email,
'token' => $request->route('token'),
]);
}
#[Post('/', name: 'password.store')]
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($status == Password::PasswordReset) {
return to_route('login')->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [__($status)],
]);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('forgot-password')]
#[Middleware('guest')]
class PasswordResetLinkController extends Controller
{
#[Get('/', name: 'password.request')]
public function create(Request $request): Response
{
return Inertia::render('auth/forgot-password', [
'status' => $request->session()->get('status'),
]);
}
#[Post('/', name: 'password.email')]
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => 'required|email',
]);
Password::sendResetLink(
$request->only('email')
);
return back()->with('status', __('A reset link will be sent if the account exists.'));
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Spatie\RouteAttributes\Attributes\Get;
class HomeController extends Controller
{
#[Get('/', name: 'home')]
public function __invoke(): RedirectResponse
{
if (auth()->check()) {
return redirect()->route('servers');
}
return redirect()->route('login');
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Server\CreateServer;
use App\Http\Resources\ServerLogResource;
use App\Http\Resources\ServerProviderResource;
use App\Http\Resources\ServerResource;
use App\Models\Server;
use App\Models\ServerProvider;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Response;
use Inertia\ResponseFactory;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('servers')]
#[Middleware(['auth', 'has-project'])]
class ServerController extends Controller
{
#[Get('/', name: 'servers')]
public function index(): Response|ResponseFactory
{
$project = user()->currentProject;
$this->authorize('viewAny', [Server::class, $project]);
$servers = $project->servers()->simplePaginate(config('web.pagination_size'));
return inertia('servers/index', [
'servers' => ServerResource::collection($servers),
'public_key' => __('servers.create.public_key_text', ['public_key' => get_public_key_content()]),
'server_providers' => ServerProviderResource::collection(ServerProvider::getByProjectId($project->id)->get()),
]);
}
#[Post('/', name: 'servers.store')]
public function store(Request $request): RedirectResponse
{
$project = user()->currentProject;
$this->authorize('create', [Server::class, $project]);
$server = app(CreateServer::class)->create(user(), $project, $request->all());
return redirect()->route('servers.show', ['server' => $server->id]);
}
#[Get('/{server}', name: 'servers.show')]
public function show(Server $server): Response|ResponseFactory
{
$this->authorize('view', $server);
return inertia('servers/show', [
'server' => ServerResource::make($server),
'logs' => ServerLogResource::collection($server->logs()->latest()->paginate(config('web.pagination_size'))),
]);
}
#[Post('/{server}/switch', name: 'servers.switch')]
public function switch(Server $server): RedirectResponse
{
$this->authorize('view', $server);
return redirect()->route('servers.show', ['server' => $server->id]);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers;
use App\Models\Server;
use App\Models\ServerLog;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('servers/{server}/logs')]
#[Middleware(['auth', 'has-project'])]
class ServerLogController extends Controller
{
#[Get('/{log}', name: 'logs.show')]
public function show(Server $server, ServerLog $log): string
{
$this->authorize('view', $log);
return $log->getContent();
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Prefix;
use Spatie\RouteAttributes\Attributes\Put;
#[Prefix('settings/password')]
#[Middleware(['auth'])]
class PasswordController extends Controller
{
#[Get('/', name: 'password.edit')]
public function edit(): Response
{
return Inertia::render('settings/password');
}
#[Put('/', name: 'password.update')]
public function update(Request $request): RedirectResponse
{
$validated = $request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back();
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Patch;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('settings/profile')]
#[Middleware(['auth'])]
class ProfileController extends Controller
{
#[Get('/', name: 'profile.edit')]
public function edit(Request $request): Response
{
return Inertia::render('settings/profile', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => $request->session()->get('status'),
]);
}
#[Patch('/', name: 'profile.update')]
public function update(Request $request): RedirectResponse
{
$this->validate($request, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
Rule::unique(User::class)->ignore(user()->id),
],
]);
$request->user()->fill($request->only('name', 'email'));
$request->user()->save();
return to_route('profile.edit');
}
#[Delete('/', name: 'profile.destroy')]
public function destroy(Request $request): RedirectResponse
{
$request->validate([
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Models\Project;
use Illuminate\Http\RedirectResponse;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('settings/projects')]
#[Middleware(['auth'])]
class ProjectController extends Controller
{
#[Post('switch/{project}', name: 'projects.switch')]
public function switch(Project $project): RedirectResponse
{
$this->authorize('view', $project);
user()->update([
'current_project_id' => $project->id,
]);
return redirect()->route('servers');
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Actions\ServerProvider\CreateServerProvider;
use App\Http\Controllers\Controller;
use App\Http\Resources\ServerProviderResource;
use App\Models\ServerProvider;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('settings/server-providers')]
#[Middleware(['auth'])]
class ServerProviderController extends Controller
{
public function index(): void {}
#[Get('/', name: 'server-providers.all')]
public function all(): ResourceCollection
{
$this->authorize('viewAny', ServerProvider::class);
return ServerProviderResource::collection(ServerProvider::getByProjectId(user()->current_project_id)->get());
}
#[Post('/', name: 'server-providers.store')]
public function store(Request $request): RedirectResponse
{
$this->authorize('create', ServerProvider::class);
app(CreateServerProvider::class)->create(user(), user()->currentProject, $request->all());
return back()->with('success', 'Server provider created.');
}
#[Get('/{serverProvider}/regions', name: 'server-providers.regions')]
public function regions(ServerProvider $serverProvider): JsonResponse
{
$this->authorize('view', $serverProvider);
return response()->json($serverProvider->provider()->regions());
}
#[Get('{serverProvider}/regions/{region}/plans', name: 'server-providers.plans')]
public function plans(ServerProvider $serverProvider, string $region): JsonResponse
{
$this->authorize('view', $serverProvider);
return response()->json($serverProvider->provider()->plans($region));
}
}

View File

@ -36,6 +36,9 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\HandleInertiaRequests::class,
\App\Http\Middleware\HandleAppearance::class,
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
],
'api' => [

View File

@ -9,10 +9,7 @@
class CanSeeProjectMiddleware
{
/**
* @return mixed
*/
public function handle(Request $request, Closure $next)
public function handle(Request $request, Closure $next): mixed
{
/** @var User $user */
$user = $request->user();

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\View;
use Symfony\Component\HttpFoundation\Response;
class HandleAppearance
{
/**
* Handle an incoming request.
*
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
View::share('appearance', $request->cookie('appearance') ?? 'system');
return $next($request);
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Http\Middleware;
use App\Http\Resources\ServerResource;
use App\Models\Server;
use App\Models\User;
use Illuminate\Foundation\Inspiring;
use Illuminate\Http\Request;
use Inertia\Middleware;
use Tighten\Ziggy\Ziggy;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that's loaded on the first page visit.
*
* @see https://inertiajs.com/server-side-setup#root-template
*
* @var string
*/
protected $rootView = 'app';
/**
* Determines the current asset version.
*
* @see https://inertiajs.com/asset-versioning
*/
public function version(Request $request): ?string
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @see https://inertiajs.com/shared-data
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');
/** @var ?User $user */
$user = $request->user();
// servers
$servers = [];
if ($user && $user->can('viewAny', [Server::class, $user->currentProject])) {
$servers = ServerResource::collection($user->currentProject?->servers);
}
return [
...parent::share($request),
'name' => config('app.name'),
'quote' => ['message' => trim($message), 'author' => trim($author)],
'auth' => [
'user' => $user,
'projects' => $user?->allProjects()->get(),
'currentProject' => $user?->currentProject,
],
'publicKeyText' => __('servers.create.public_key_text', ['public_key' => get_public_key_content()]),
'projectServers' => $servers,
'configs' => config('core'),
'ziggy' => fn (): array => [
...(new Ziggy)->toArray(),
'location' => $request->url(),
],
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
];
}
}

View File

@ -2,16 +2,14 @@
namespace App\Http\Middleware;
use App\Models\Project;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
class HasProjectMiddleware
{
/**
* @return mixed
*/
public function handle(Request $request, Closure $next)
public function handle(Request $request, Closure $next): mixed
{
/** @var ?User $user */
$user = $request->user();
@ -21,7 +19,7 @@ public function handle(Request $request, Closure $next)
if (! $user->currentProject) {
if ($user->allProjects()->count() > 0) {
/** @var \App\Models\Project $firstProject */
/** @var Project $firstProject */
$firstProject = $user->allProjects()->first();
$user->current_project_id = $firstProject->id;
$user->save();

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Resources;
use App\Models\ServerLog;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin ServerLog */
class ServerLogResource extends JsonResource
{
/**
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'server_id' => $this->server_id,
'site_id' => $this->site_id,
'type' => $this->type,
'name' => $this->name,
'disk' => $this->disk,
'is_remote' => $this->is_remote,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'created_at_by_timezone' => $this->created_at_by_timezone,
'updated_at_by_timezone' => $this->updated_at_by_timezone,
];
}
}

View File

@ -22,6 +22,8 @@ public function toArray(Request $request): array
'provider' => $this->provider,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'created_at_by_timezone' => $this->created_at_by_timezone,
'updated_at_by_timezone' => $this->updated_at_by_timezone,
];
}
}

View File

@ -38,8 +38,11 @@ public function toArray(Request $request): array
'progress_step' => $this->progress_step,
'updates' => $this->updates,
'last_update_check' => $this->last_update_check,
'status_color' => $this->getStatusColor(),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'created_at_by_timezone' => $this->created_at_by_timezone,
'updated_at_by_timezone' => $this->updated_at_by_timezone,
];
}
}