Migrate HickLab3D dashboard and add production auto deployment
Deploy HickLab3D Dashboard / Deploy to production (push) Failing after 19s
Deploy HickLab3D Dashboard / Deploy to production (push) Failing after 19s
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$credentials = $request->validate(['username' => ['required', 'string'], 'password' => ['required', 'string']]);
|
||||
$user = User::where('username', $credentials['username'])->first();
|
||||
if (! $user || $user->disabled || ! Auth::attempt($credentials)) {
|
||||
return back()->withErrors(['username' => 'These credentials do not match an active account.'])->onlyInput('username');
|
||||
}
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('dashboard'));
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Bambuddy;
|
||||
use Illuminate\View\View;
|
||||
use Throwable;
|
||||
|
||||
class BambuddyController extends Controller
|
||||
{
|
||||
public function __invoke(Bambuddy $bambuddy): View
|
||||
{
|
||||
$printers = [];
|
||||
$integrationError = null;
|
||||
|
||||
try {
|
||||
$printers = $bambuddy->printers();
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
$integrationError = 'BamBuddy is unavailable. Check its API key and connection.';
|
||||
}
|
||||
|
||||
return view('printers.index', compact('printers', 'integrationError'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Filament;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): View
|
||||
{
|
||||
return view('dashboard', [
|
||||
'filamentCount' => Filament::count(),
|
||||
'userCount' => $request->user()->role === 'admin' ? User::count() : null,
|
||||
'recentFilaments' => Filament::with(['prices' => fn ($query) => $query->orderByDesc('recordedAt')->limit(1)])->orderByDesc('id')->limit(5)->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\DryerClient;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Throwable;
|
||||
|
||||
class DryerController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('dryer');
|
||||
}
|
||||
|
||||
public function status(DryerClient $dryer): JsonResponse
|
||||
{
|
||||
try {
|
||||
return response()->json(['status' => $dryer->status(), 'history' => $dryer->history()]);
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return response()->json(['message' => 'Dryer is unavailable.'], 503);
|
||||
}
|
||||
}
|
||||
|
||||
public function control(Request $request, DryerClient $dryer): JsonResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
$data = $request->validate([
|
||||
'dryerOn' => ['required', 'boolean'],
|
||||
'profile' => ['required_if:dryerOn,1', 'nullable', 'string', 'in:PLA,PETG,ABS,Nylon,Custom'],
|
||||
'targetTemperature' => ['required_if:dryerOn,1', 'nullable', 'integer', 'between:45,70'],
|
||||
'dryingTime' => ['required_if:dryerOn,1', 'nullable', 'integer', 'between:5,2880'],
|
||||
]);
|
||||
try {
|
||||
$result = $data['dryerOn']
|
||||
? $dryer->start($data['profile'], $data['targetTemperature'], $data['dryingTime'])
|
||||
: $dryer->stop();
|
||||
|
||||
return response()->json($result);
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return response()->json(['message' => 'Dryer is unavailable.'], 503);
|
||||
}
|
||||
}
|
||||
|
||||
public function acknowledge(Request $request, DryerClient $dryer): JsonResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
try {
|
||||
return response()->json($dryer->acknowledge());
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return response()->json(['message' => 'Dryer fault could not be acknowledged.'], 503);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\EtsyConnection;
|
||||
use App\Services\Etsy;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Throwable;
|
||||
|
||||
class EtsyController extends Controller
|
||||
{
|
||||
public function index(Request $request, Etsy $etsy): View
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
$connection = EtsyConnection::first();
|
||||
$receipts = [];
|
||||
$listings = [];
|
||||
$integrationError = null;
|
||||
|
||||
if ($connection && $etsy->configured()) {
|
||||
try {
|
||||
$receipts = $etsy->receipts($connection)['results'] ?? [];
|
||||
$listings = $etsy->listings($connection)['results'] ?? [];
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
$integrationError = 'Etsy data is temporarily unavailable. Reconnect the shop if this continues.';
|
||||
}
|
||||
}
|
||||
|
||||
return view('etsy', compact('connection', 'receipts', 'listings', 'integrationError') + ['configured' => $etsy->configured()]);
|
||||
}
|
||||
|
||||
public function connect(Request $request, Etsy $etsy): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
if (! $etsy->configured()) {
|
||||
return back()->withErrors(['etsy' => 'Configure the Etsy app key and shared secret first.']);
|
||||
}
|
||||
|
||||
$state = bin2hex(random_bytes(24));
|
||||
$verifier = rtrim(strtr(base64_encode(random_bytes(48)), '+/', '-_'), '=');
|
||||
$request->session()->put('etsy_oauth', ['state' => $state, 'verifier' => $verifier, 'started' => time()]);
|
||||
|
||||
return redirect()->away($etsy->authorizationUrl($state, $verifier));
|
||||
}
|
||||
|
||||
public function callback(Request $request, Etsy $etsy): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
$pending = $request->session()->pull('etsy_oauth');
|
||||
if (! is_array($pending) || time() - ($pending['started'] ?? 0) > 600
|
||||
|| ! is_string($request->query('state'))
|
||||
|| ! hash_equals($pending['state'] ?? '', $request->query('state'))
|
||||
|| ! is_string($request->query('code'))) {
|
||||
return redirect()->route('etsy.index')->withErrors(['etsy' => 'Etsy connection expired or was not verified. Please try again.']);
|
||||
}
|
||||
|
||||
try {
|
||||
$connection = $etsy->connect($request->query('code'), $pending['verifier']);
|
||||
|
||||
return redirect()->route('etsy.index')->with('status', $connection->shop_name.' connected to Etsy.');
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return redirect()->route('etsy.index')->withErrors(['etsy' => 'Etsy rejected the connection. Check the app credentials and callback URL.']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Filament;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class FilamentController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('filaments.index', ['filaments' => Filament::with(['prices' => fn ($query) => $query->orderByDesc('recordedAt')->limit(10)])->orderBy('filamentName')->paginate(20)]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
|
||||
return view('filaments.create');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
$data = $request->validate([
|
||||
'filamentName' => ['required', 'string', 'max:255'],
|
||||
'amazonUrl' => ['required', 'url:http,https', 'max:2048'],
|
||||
'brand' => ['nullable', 'string', 'max:255'],
|
||||
'material' => ['nullable', 'string', 'max:255'],
|
||||
'color' => ['nullable', 'string', 'max:255'],
|
||||
'filamentWeight' => ['nullable', 'numeric', 'min:0'],
|
||||
'itemDiameter' => ['nullable', 'numeric', 'min:0'],
|
||||
]);
|
||||
$host = strtolower((string) parse_url($data['amazonUrl'], PHP_URL_HOST));
|
||||
if (! in_array($host, ['amazon.co.uk', 'www.amazon.co.uk', 'amazon.com', 'www.amazon.com'], true)) {
|
||||
return back()->withErrors(['amazonUrl' => 'Enter an Amazon UK or US product URL.'])->withInput();
|
||||
}
|
||||
Filament::create([...$data, 'userId' => $request->user()->id]);
|
||||
|
||||
return redirect()->route('filaments.index')->with('status', 'Filament added. Price collection runs on the schedule.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Filament $filament): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
$filament->delete();
|
||||
|
||||
return redirect()->route('filaments.index')->with('status', 'Filament removed.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Printer;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PrinterController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$printers = Printer::where('userId', $request->user()->id)
|
||||
->with(['telemetry' => fn ($query) => $query->orderByDesc('recordedAt')->limit(1)])
|
||||
->orderBy('printerName')->get();
|
||||
|
||||
return view('printers.index', compact('printers'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('printers.create');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'printerName' => ['required', 'string', 'max:255'],
|
||||
'printerIp' => ['required', 'ip'],
|
||||
'serialNumber' => ['required', 'string', 'max:255', 'unique:bambuPrinters,serialNumber'],
|
||||
'mqttPassword' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
Printer::create([...$data, 'userId' => $request->user()->id]);
|
||||
|
||||
return redirect()->route('printers.index')->with('status', 'Printer added.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Printer $printer): RedirectResponse
|
||||
{
|
||||
abort_unless($printer->userId === $request->user()->id, 404);
|
||||
$printer->delete();
|
||||
|
||||
return redirect()->route('printers.index')->with('status', 'Printer removed.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Parcel2Go;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
use Throwable;
|
||||
|
||||
class ShippingController extends Controller
|
||||
{
|
||||
public function index(Request $request, Parcel2Go $parcel2Go): View
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
try {
|
||||
$account = $parcel2Go->fetch('me/detail');
|
||||
$balance = $parcel2Go->fetch('me/prepay/balance');
|
||||
$orders = $parcel2Go->fetch('me/orders/detail');
|
||||
|
||||
return view('shipping', compact('account', 'balance', 'orders') + [
|
||||
'quotes' => $request->session()->get('shipping_quotes', []),
|
||||
'collectionDates' => $request->session()->get('shipping_collection_dates', []),
|
||||
'selectedService' => $request->session()->get('shipping_selected_service'),
|
||||
'bookingResult' => $request->session()->get('shipping_booking_result'),
|
||||
'bookingPending' => $request->session()->get('shipping_booking_pending', false),
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return view('shipping', [
|
||||
'account' => [], 'balance' => [], 'orders' => [],
|
||||
'quotes' => $request->session()->get('shipping_quotes', []),
|
||||
'collectionDates' => $request->session()->get('shipping_collection_dates', []),
|
||||
'selectedService' => $request->session()->get('shipping_selected_service'),
|
||||
'bookingResult' => $request->session()->get('shipping_booking_result'),
|
||||
'bookingPending' => $request->session()->get('shipping_booking_pending', false),
|
||||
'integrationError' => 'Parcel2Go is unavailable. Check API credentials and connectivity.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function quote(Request $request, Parcel2Go $parcel2Go): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
if ($request->session()->get('shipping_booking_pending')) {
|
||||
return redirect()->route('shipping')->withErrors(['collection' => 'A booking outcome is uncertain. Check your Parcel2Go account before trying again.']);
|
||||
}
|
||||
$details = $request->validate([
|
||||
'collection_postcode' => ['required', 'string', 'max:12'],
|
||||
'delivery_postcode' => ['required', 'string', 'max:12'],
|
||||
'weight' => ['required', 'numeric', 'gt:0', 'lte:30'],
|
||||
'length' => ['required', 'numeric', 'gt:0', 'lte:200'],
|
||||
'width' => ['required', 'numeric', 'gt:0', 'lte:200'],
|
||||
'height' => ['required', 'numeric', 'gt:0', 'lte:200'],
|
||||
'value' => ['required', 'numeric', 'gte:0', 'lte:10000'],
|
||||
]);
|
||||
try {
|
||||
$quotes = $parcel2Go->quote($details)['Quotes'] ?? [];
|
||||
$request->session()->put('shipping_quote', $details);
|
||||
$request->session()->put('shipping_quotes', $quotes);
|
||||
$request->session()->forget(['shipping_collection_dates', 'shipping_selected_service']);
|
||||
|
||||
return redirect()->route('shipping')->with('status', count($quotes).' services available.');
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return redirect()->route('shipping')->withErrors(['quote' => 'Parcel2Go could not quote this parcel. Check the postcodes and parcel details.']);
|
||||
}
|
||||
}
|
||||
|
||||
public function collectionDates(Request $request, Parcel2Go $parcel2Go): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
$data = $request->validate([
|
||||
'service' => ['required', 'string', 'max:200'],
|
||||
'sender_street' => ['required', 'string', 'max:200'],
|
||||
'sender_town' => ['required', 'string', 'max:100'],
|
||||
]);
|
||||
$details = $request->session()->get('shipping_quote');
|
||||
$quote = collect($request->session()->get('shipping_quotes', []))->first(fn ($quote) => ($quote['Service']['Slug'] ?? null) === $data['service'] && ($quote['Service']['CollectionType'] ?? null) === 'Collection');
|
||||
if (! $details || ! $quote) {
|
||||
return redirect()->route('shipping')->withErrors(['service' => 'Select a collection service from a current quote.']);
|
||||
}
|
||||
|
||||
try {
|
||||
$dates = $parcel2Go->collectionDates($data['service'], $data['sender_street'], $data['sender_town'], $details['collection_postcode'])['CollectionDates'] ?? [];
|
||||
$request->session()->put('shipping_collection_dates', $dates);
|
||||
$request->session()->put('shipping_selected_service', $data['service']);
|
||||
$request->session()->put('shipping_sender_location', $data);
|
||||
|
||||
return redirect()->route('shipping')->with('status', 'Choose a collection date and enter the addresses.');
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return redirect()->route('shipping')->withErrors(['collection' => 'Parcel2Go could not return collection dates for this service.']);
|
||||
}
|
||||
}
|
||||
|
||||
public function bookCollection(Request $request, Parcel2Go $parcel2Go): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
if ($request->session()->get('shipping_booking_pending')) {
|
||||
return redirect()->route('shipping')->withErrors(['collection' => 'A booking outcome is uncertain. Check your Parcel2Go account before trying again.']);
|
||||
}
|
||||
$booking = $request->validate([
|
||||
'collection_date' => ['required', 'date', 'after:now'],
|
||||
'sender_name' => ['required', 'string', 'max:100'],
|
||||
'sender_email' => ['required', 'email', 'max:150'],
|
||||
'sender_phone' => ['required', 'string', 'max:30'],
|
||||
'sender_property' => ['required', 'string', 'max:100'],
|
||||
'sender_street' => ['required', 'string', 'max:200'],
|
||||
'sender_town' => ['required', 'string', 'max:100'],
|
||||
'recipient_name' => ['required', 'string', 'max:100'],
|
||||
'recipient_email' => ['required', 'email', 'max:150'],
|
||||
'recipient_phone' => ['required', 'string', 'max:30'],
|
||||
'recipient_property' => ['required', 'string', 'max:100'],
|
||||
'recipient_street' => ['required', 'string', 'max:200'],
|
||||
'recipient_town' => ['required', 'string', 'max:100'],
|
||||
'contents' => ['required', 'string', 'max:200'],
|
||||
]);
|
||||
$details = $request->session()->get('shipping_quote');
|
||||
$service = $request->session()->get('shipping_selected_service');
|
||||
$dates = $request->session()->get('shipping_collection_dates', []);
|
||||
$senderLocation = $request->session()->get('shipping_sender_location', []);
|
||||
$selectedQuote = collect($request->session()->get('shipping_quotes', []))
|
||||
->first(fn ($quote) => ($quote['Service']['Slug'] ?? null) === $service && ($quote['Service']['CollectionType'] ?? null) === 'Collection');
|
||||
if (! $details || ! $service || ! collect($dates)->contains(fn ($date) => ($date['CollectionDate'] ?? null) === $booking['collection_date'])
|
||||
|| ! $selectedQuote || ! empty($selectedQuote['RequiresCustoms']) || ! empty($selectedQuote['RequiresCommercialInvoice'])
|
||||
|| ($senderLocation['sender_street'] ?? null) !== $booking['sender_street'] || ($senderLocation['sender_town'] ?? null) !== $booking['sender_town']) {
|
||||
return redirect()->route('shipping')->withErrors(['collection' => 'Choose an available collection date for the quoted address.']);
|
||||
}
|
||||
|
||||
try {
|
||||
$request->session()->put('shipping_booking_pending', true);
|
||||
$result = $parcel2Go->bookCollection($details, $service, $booking);
|
||||
if (empty($result['OrderId'])) {
|
||||
throw new \RuntimeException('Parcel2Go did not return an order reference.');
|
||||
}
|
||||
$request->session()->forget(['shipping_quote', 'shipping_quotes', 'shipping_collection_dates', 'shipping_selected_service', 'shipping_sender_location', 'shipping_booking_pending']);
|
||||
|
||||
return redirect()->route('shipping')->with('shipping_booking_result', $result)->with('status', 'Collection order created. Complete payment with Parcel2Go.');
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return redirect()->route('shipping')->withErrors(['collection' => 'Booking outcome is uncertain. Check your Parcel2Go account before trying again.']);
|
||||
}
|
||||
}
|
||||
|
||||
public function resetBooking(Request $request): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
$request->validate(['checked_account' => ['accepted']]);
|
||||
$request->session()->forget(['shipping_quote', 'shipping_quotes', 'shipping_collection_dates', 'shipping_selected_service', 'shipping_sender_location', 'shipping_booking_pending']);
|
||||
|
||||
return redirect()->route('shipping')->with('status', 'Booking workflow reset. Start with a fresh quote.');
|
||||
}
|
||||
|
||||
public function label(Request $request, Parcel2Go $parcel2Go, string $orderLineId): Response
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
abort_unless(ctype_digit($orderLineId), 404);
|
||||
$orders = $parcel2Go->fetch('me/orders/detail')['Orders'] ?? [];
|
||||
$order = collect($orders)->first(fn ($order) => (string) ($order['OrderLineId'] ?? '') === $orderLineId);
|
||||
abort_unless($order && ! empty($order['Hash']), 404);
|
||||
$pdf = $parcel2Go->labelPdf($orderLineId, $order['Hash']);
|
||||
|
||||
return response($pdf, 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'attachment; filename="parcel2go-'.$orderLineId.'.pdf"',
|
||||
'Cache-Control' => 'private, no-store',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAdmin($request);
|
||||
|
||||
return view('users.index', ['users' => User::orderBy('username')->paginate(20)]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAdmin($request);
|
||||
$data = $request->validate([
|
||||
'username' => ['required', 'string', 'max:255', 'unique:users,username'],
|
||||
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
||||
'role' => ['required', Rule::in(['admin', 'user'])],
|
||||
'password' => ['required', 'string', 'min:12'],
|
||||
'alertEmails' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
User::create($data);
|
||||
|
||||
return back()->with('status', 'User created.');
|
||||
}
|
||||
|
||||
public function update(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->authorizeAdmin($request);
|
||||
$data = $request->validate([
|
||||
'email' => ['sometimes', 'email', Rule::unique('users', 'email')->ignore($user->id)],
|
||||
'role' => ['sometimes', Rule::in(['admin', 'user'])],
|
||||
'disabled' => ['sometimes', 'boolean'],
|
||||
'alertEmails' => ['sometimes', 'boolean'],
|
||||
'password' => ['nullable', 'string', 'min:12'],
|
||||
]);
|
||||
if (empty($data['password'])) {
|
||||
unset($data['password']);
|
||||
}
|
||||
if ($user->id === $request->user()->id && (($data['disabled'] ?? false) || ($data['role'] ?? 'admin') !== 'admin')) {
|
||||
return back()->withErrors(['user' => 'You cannot disable or demote your own admin account.']);
|
||||
}
|
||||
$user->update($data);
|
||||
|
||||
return back()->with('status', 'User updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->authorizeAdmin($request);
|
||||
abort_if($user->id === $request->user()->id, 422, 'You cannot delete your own account.');
|
||||
$user->delete();
|
||||
|
||||
return back()->with('status', 'User deleted.');
|
||||
}
|
||||
|
||||
private function authorizeAdmin(Request $request): void
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureAccountActive
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if ($request->user()?->disabled) {
|
||||
auth()->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('login')->withErrors(['username' => 'This account is disabled.']);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user