Simplify integration settings management
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 1m48s
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 1m48s
This commit is contained in:
@@ -28,6 +28,7 @@ class EtsySettingsController extends Controller
|
|||||||
return view('settings.index', [
|
return view('settings.index', [
|
||||||
'storedCredentials' => $credentials !== null,
|
'storedCredentials' => $credentials !== null,
|
||||||
'configured' => $etsy->configured(),
|
'configured' => $etsy->configured(),
|
||||||
|
'shopConnected' => EtsyConnection::query()->exists(),
|
||||||
'redirectUri' => $etsy->redirectUri(),
|
'redirectUri' => $etsy->redirectUri(),
|
||||||
'shopName' => $credentials?->shop_name ?: config('services.etsy.shop_name', 'HickLab3D'),
|
'shopName' => $credentials?->shop_name ?: config('services.etsy.shop_name', 'HickLab3D'),
|
||||||
'integrations' => [
|
'integrations' => [
|
||||||
|
|||||||
@@ -1,114 +1,130 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', 'Settings')
|
@section('title', 'Settings')
|
||||||
@section('content')
|
@section('content')
|
||||||
<div>
|
<div class="flex flex-wrap items-end justify-between gap-4">
|
||||||
<p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Integrations</p>
|
<div><p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Workspace connections</p><h2 class="mt-2 text-2xl font-bold">Integration settings</h2><p class="mt-2 max-w-2xl text-sm text-slate-400">Manage the services behind your orders, shipping and workshop. Saved credentials are encrypted in the database.</p></div>
|
||||||
<h2 class="mt-2 text-2xl font-bold">Integration settings</h2>
|
<span class="rounded-full border border-slate-700 px-3 py-1.5 text-xs text-slate-400">Administrators only</span>
|
||||||
<p class="mt-1 text-sm text-slate-400">Manage the connections used by your shop and workshop. Saved values are encrypted in the database.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
|
<nav class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4" aria-label="Integration settings sections">
|
||||||
|
@foreach([
|
||||||
|
['etsy', 'Etsy shop', $configured && $shopConnected, $storedCredentials, 'Orders and listings'],
|
||||||
|
['parcel2go', 'Parcel2Go', $integrations['parcel2go']['configured'], $integrations['parcel2go']['stored'], 'Shipping and labels'],
|
||||||
|
['bambuddy', 'BamBuddy', $integrations['bambuddy']['configured'], $integrations['bambuddy']['stored'], 'Print farm'],
|
||||||
|
['dryer', 'Filament dryer', $integrations['dryer']['configured'], $integrations['dryer']['stored'], 'Drying controls'],
|
||||||
|
] as [$section, $name, $isConfigured, $isStored, $description])
|
||||||
|
<a href="#{{ $section }}" class="group rounded-2xl border border-slate-800 bg-slate-900 p-4 transition-colors hover:border-brand/60 hover:bg-slate-800/70">
|
||||||
|
<span class="flex items-center justify-between gap-2"><strong class="text-sm text-white">{{ $name }}</strong><span aria-hidden="true" class="text-slate-500 group-hover:text-brand">↗</span></span>
|
||||||
|
<span class="mt-1 block text-xs text-slate-400">{{ $description }}</span>
|
||||||
|
<span @class(['mt-4 inline-flex rounded-full px-2.5 py-1 text-xs font-semibold', 'bg-emerald-500/10 text-emerald-300' => $isConfigured, 'bg-amber-500/10 text-amber-200' => ! $isConfigured])>{{ $section === 'etsy' && $configured ? ($shopConnected ? 'Shop connected' : 'Connect shop') : ($isConfigured ? ($isStored ? 'Saved in dashboard' : 'Using server settings') : 'Setup needed') }}</span>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section id="etsy" class="scroll-mt-6 rounded-2xl border border-slate-800 bg-slate-900 p-6">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold">Etsy shop</h3>
|
<h3 class="text-lg font-semibold">Etsy shop</h3>
|
||||||
<p class="mt-1 text-sm text-slate-400">{{ $storedCredentials ? 'Credentials saved in the database.' : ($configured ? 'Using server credentials. Save them here to manage them in the dashboard.' : 'No credentials saved yet.') }}</p>
|
<p class="mt-1 text-sm text-slate-400">{{ $storedCredentials ? 'App credentials saved in dashboard.' : ($configured ? 'Using server app credentials.' : 'Add your Etsy app credentials.') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="rounded-full px-3 py-1 text-xs font-semibold {{ $configured ? 'bg-emerald-500/10 text-emerald-300' : 'bg-amber-500/10 text-amber-200' }}">{{ $configured ? 'Configured' : 'Setup needed' }}</span>
|
<span @class(['rounded-full px-3 py-1 text-xs font-semibold', 'bg-emerald-500/10 text-emerald-300' => $configured && $shopConnected, 'bg-amber-500/10 text-amber-200' => ! ($configured && $shopConnected)])>{{ $configured && $shopConnected ? 'Shop connected' : ($configured ? 'Connect shop' : 'Setup needed') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="{{ route('settings.etsy.update') }}" class="mt-6 grid gap-5 md:grid-cols-2">
|
<form method="post" action="{{ route('settings.etsy.update') }}" class="mt-6 grid gap-5 md:grid-cols-2">
|
||||||
@csrf
|
@csrf
|
||||||
@method('PUT')
|
@method('PUT')
|
||||||
<label class="grid gap-2 text-sm text-slate-300">
|
<label class="grid gap-2 text-sm text-slate-300">
|
||||||
Etsy app keystring
|
App keystring
|
||||||
<input name="keystring" type="password" autocomplete="new-password" maxlength="255" placeholder="{{ $configured ? 'Leave blank to keep current key' : 'Enter your app keystring' }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500" {{ $configured ? '' : 'required' }}>
|
<input name="keystring" type="password" autocomplete="new-password" maxlength="255" placeholder="{{ $configured ? 'Saved — leave blank to keep' : 'Enter Etsy app keystring' }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500" {{ $configured ? '' : 'required' }}>
|
||||||
</label>
|
</label>
|
||||||
<label class="grid gap-2 text-sm text-slate-300">
|
<label class="grid gap-2 text-sm text-slate-300">
|
||||||
Etsy shared secret
|
Shared secret
|
||||||
<input name="shared_secret" type="password" autocomplete="new-password" maxlength="255" placeholder="{{ $configured ? 'Leave blank to keep current secret' : 'Enter your shared secret' }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500" {{ $configured ? '' : 'required' }}>
|
<input name="shared_secret" type="password" autocomplete="new-password" maxlength="255" placeholder="{{ $configured ? 'Saved — leave blank to keep' : 'Enter Etsy shared secret' }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500" {{ $configured ? '' : 'required' }}>
|
||||||
</label>
|
</label>
|
||||||
<label class="grid gap-2 text-sm text-slate-300">
|
<label class="grid gap-2 text-sm text-slate-300">
|
||||||
Shop name
|
Shop name
|
||||||
<input name="shop_name" type="text" maxlength="255" value="{{ old('shop_name', $shopName) }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100">
|
<input name="shop_name" type="text" maxlength="255" value="{{ old('shop_name', $shopName) }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100">
|
||||||
</label>
|
</label>
|
||||||
<label class="grid gap-2 text-sm text-slate-300">
|
<details class="rounded-xl border border-slate-800 bg-slate-950/60 p-4 md:col-span-2" @if($errors->has('redirect_uri')) open @endif>
|
||||||
OAuth callback URL
|
<summary class="cursor-pointer text-sm font-semibold text-slate-200">Advanced: OAuth callback</summary>
|
||||||
<input name="redirect_uri" type="url" maxlength="2048" value="{{ old('redirect_uri', $redirectUri) }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100">
|
<label class="mt-4 grid gap-2 text-sm text-slate-300">Callback URL<input name="redirect_uri" type="url" maxlength="2048" value="{{ old('redirect_uri', $redirectUri) }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
||||||
</label>
|
<p class="mt-2 text-xs text-slate-400">Register this exact HTTPS URL in your Etsy developer app.</p>
|
||||||
|
</details>
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<button class="rounded-xl bg-brand px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Save credentials</button>
|
<button class="rounded-xl bg-brand px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Save Etsy settings</button>
|
||||||
|
@if($configured)<a href="{{ route('etsy.index') }}" class="ml-2 inline-flex rounded-xl border border-slate-700 px-5 py-3 text-sm font-semibold text-slate-200 hover:border-brand">{{ $shopConnected ? 'Open Etsy shop' : 'Connect Etsy shop' }}</a>@endif
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<p class="mt-4 text-xs text-slate-500">Blank credential fields keep the current values. Saving imports any server credentials you have not entered here. Changing the app key disconnects the current Etsy shop. The callback must match the URL registered with Etsy.</p>
|
<p class="mt-4 text-xs text-slate-500">Leave saved credentials blank to keep them. Saving imports server credentials if needed. Changing the app key disconnects the shop until you reconnect it.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="grid gap-5 xl:grid-cols-2">
|
<div class="grid gap-5 xl:grid-cols-2">
|
||||||
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
|
<section id="parcel2go" class="scroll-mt-6 rounded-2xl border border-slate-800 bg-slate-900 p-6 xl:col-span-2">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div><h3 class="text-lg font-semibold">Parcel2Go</h3><p class="mt-1 text-sm text-slate-400">Quotes, labels, shipments and collections.</p></div>
|
<div><h3 class="text-lg font-semibold">Parcel2Go</h3><p class="mt-1 text-sm text-slate-400">Client Credentials for shipping, account history and labels.</p></div>
|
||||||
<span @class(['rounded-full px-3 py-1 text-xs font-semibold', 'bg-emerald-500/10 text-emerald-300' => $integrations['parcel2go']['configured'], 'bg-amber-500/10 text-amber-200' => ! $integrations['parcel2go']['configured']])>{{ $integrations['parcel2go']['configured'] ? 'Configured' : 'Setup needed' }}</span>
|
<span @class(['rounded-full px-3 py-1 text-xs font-semibold', 'bg-emerald-500/10 text-emerald-300' => $integrations['parcel2go']['configured'], 'bg-amber-500/10 text-amber-200' => ! $integrations['parcel2go']['configured']])>{{ $integrations['parcel2go']['configured'] ? ($integrations['parcel2go']['stored'] ? 'Saved in dashboard' : 'Using server settings') : 'Setup needed' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-xs text-slate-500">{{ $integrations['parcel2go']['stored'] ? 'Saved in the database.' : 'Using server values where available. Save to import them.' }}</p>
|
<p class="mt-4 text-sm text-slate-400">Use the Client ID and secret from the Parcel2Go client that worked in your previous dashboard. Leave saved credentials blank to keep them.</p>
|
||||||
<form method="post" action="{{ route('settings.integrations.update', 'parcel2go') }}" class="mt-5 grid gap-4">
|
<form method="post" action="{{ route('settings.integrations.update', 'parcel2go') }}" class="mt-5 grid gap-4 lg:grid-cols-2">
|
||||||
@csrf @method('PUT')
|
@csrf @method('PUT')
|
||||||
<label class="grid gap-2 text-sm text-slate-300">Client ID<input name="client_id" type="password" autocomplete="new-password" maxlength="255" placeholder="Leave blank to keep current value" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
<label class="grid gap-2 text-sm text-slate-300">Client ID<input name="client_id" type="password" autocomplete="new-password" maxlength="255" placeholder="{{ $integrations['parcel2go']['configured'] ? 'Saved — leave blank to keep' : 'Enter Client ID' }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
||||||
<label class="grid gap-2 text-sm text-slate-300">Client secret<input name="client_secret" type="password" autocomplete="new-password" maxlength="255" placeholder="Leave blank to keep current value" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
<label class="grid gap-2 text-sm text-slate-300">Client secret<input name="client_secret" type="password" autocomplete="new-password" maxlength="255" placeholder="{{ $integrations['parcel2go']['configured'] ? 'Saved — leave blank to keep' : 'Enter client secret' }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
||||||
<label class="grid gap-2 text-sm text-slate-300">OAuth token URL<input name="auth_url" type="url" maxlength="2048" value="{{ old('auth_url', $integrations['parcel2go']['auth_url']) }}" placeholder="https://…" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
<label class="grid gap-2 text-sm text-slate-300 lg:col-span-2">Collection postcode<input name="collection_postcode" maxlength="12" value="{{ old('collection_postcode', $integrations['parcel2go']['collection_postcode']) }}" placeholder="Used when quoting Etsy orders" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
||||||
|
<details class="rounded-xl border border-slate-800 bg-slate-950/60 p-4 lg:col-span-2" @if($errors->has('auth_url') || $errors->has('base_url') || $errors->has('scope') || $errors->has('profile_client_id')) open @endif>
|
||||||
|
<summary class="cursor-pointer text-sm font-semibold text-slate-200">Advanced: API endpoints and access</summary>
|
||||||
|
<div class="mt-4 grid gap-4 lg:grid-cols-2">
|
||||||
|
<label class="grid gap-2 text-sm text-slate-300">OAuth token URL<input name="auth_url" type="url" maxlength="2048" value="{{ old('auth_url', $integrations['parcel2go']['auth_url']) }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
||||||
<label class="grid gap-2 text-sm text-slate-300">API base URL<input name="base_url" type="url" maxlength="2048" value="{{ old('base_url', $integrations['parcel2go']['base_url']) }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
<label class="grid gap-2 text-sm text-slate-300">API base URL<input name="base_url" type="url" maxlength="2048" value="{{ old('base_url', $integrations['parcel2go']['base_url']) }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
||||||
<label class="grid gap-2 text-sm text-slate-300">Collection postcode<input name="collection_postcode" maxlength="12" value="{{ old('collection_postcode', $integrations['parcel2go']['collection_postcode']) }}" placeholder="Used for Etsy order shipping quotes" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
<label class="grid gap-2 text-sm text-slate-300">Requested API scope<select name="scope" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100">@foreach(['public-api' => 'public-api (recommended)', 'public-api my-profile' => 'public-api + my-profile', 'public-api payment' => 'public-api + payment', 'public-api payment my-profile' => 'public-api + payment + my-profile'] as $scope => $label)<option value="{{ $scope }}" @selected(old('scope', $integrations['parcel2go']['scope']) === $scope)>{{ $label }}</option>@endforeach</select></label>
|
||||||
<label class="grid gap-2 text-sm text-slate-300">Authorization Code Client ID<input name="profile_client_id" type="password" autocomplete="new-password" maxlength="255" placeholder="{{ $integrations['parcel2go']['profile_client_configured'] ? 'Saved — leave blank to keep it' : 'Enter the new Authorization Code Client ID' }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
<label class="grid gap-2 text-sm text-slate-300">Optional Authorization Code Client ID<input name="profile_client_id" type="password" autocomplete="new-password" maxlength="255" placeholder="{{ $integrations['parcel2go']['profile_client_configured'] ? 'Saved — leave blank to keep' : 'Only needed for optional consent' }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
||||||
<label class="grid gap-2 text-sm text-slate-300">API access
|
</div>
|
||||||
<select name="scope" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100">
|
</details>
|
||||||
@foreach(['public-api' => 'Basic API access (also tries account history)', 'public-api my-profile' => 'Basic API and my-profile', 'public-api payment' => 'Basic API and payments', 'public-api payment my-profile' => 'Basic API, payments and my-profile'] as $scope => $label)
|
<div class="lg:col-span-2"><button class="rounded-xl bg-brand px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Save Parcel2Go settings</button></div>
|
||||||
<option value="{{ $scope }}" @selected(old('scope', $integrations['parcel2go']['scope']) === $scope)>{{ $label }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<p class="text-xs text-slate-500">The dashboard tries shipments, account details and balance with your Client Credentials connection, as the previous dashboard did. If Parcel2Go denies history, check that these credentials belong to the same client that worked before.</p>
|
|
||||||
<button class="w-fit rounded-xl bg-brand px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Save Parcel2Go</button>
|
|
||||||
</form>
|
</form>
|
||||||
<div class="mt-5 grid gap-3 rounded-xl border border-slate-700 bg-slate-950 p-4 text-sm">
|
<div class="mt-5 grid gap-3 rounded-xl border border-slate-700 bg-slate-950 p-4 text-sm">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2"><p class="font-semibold">Optional Parcel2Go account consent</p><span @class(['rounded-full px-2 py-1 text-xs font-semibold', 'bg-emerald-500/10 text-emerald-300' => $integrations['parcel2go']['profile_connected'], 'bg-amber-500/10 text-amber-200' => ! $integrations['parcel2go']['profile_connected']])>{{ $integrations['parcel2go']['profile_connected'] ? 'Connected' : 'Not connected' }}</span></div>
|
<div class="flex flex-wrap items-center justify-between gap-2"><p class="font-semibold">Optional Parcel2Go account consent</p><span @class(['rounded-full px-2 py-1 text-xs font-semibold', 'bg-emerald-500/10 text-emerald-300' => $integrations['parcel2go']['profile_connected'], 'bg-amber-500/10 text-amber-200' => ! $integrations['parcel2go']['profile_connected']])>{{ $integrations['parcel2go']['profile_connected'] ? 'Connected' : 'Not connected' }}</span></div>
|
||||||
<p class="text-slate-400">Create an <strong>Authorization Code</strong> client in Parcel2Go. Set Application URL to <code>{{ url('/') }}</code> and Redirect URI to this exact address:</p>
|
<p class="text-slate-400">The dashboard tries shipment history and account details with Client Credentials first. Authorization Code consent is optional.</p>
|
||||||
|
<details class="border-t border-slate-800 pt-3" @if($errors->has('parcel2go')) open @endif>
|
||||||
|
<summary class="cursor-pointer font-semibold text-brand">Set up optional account consent</summary>
|
||||||
|
<div class="mt-3 grid gap-3 text-slate-400">
|
||||||
|
<p>Create an Authorization Code client in Parcel2Go. Set Application URL to <code>{{ url('/') }}</code> and register this exact Redirect URI:</p>
|
||||||
<code class="overflow-x-auto rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-slate-200">{{ $integrations['parcel2go']['callback_url'] }}</code>
|
<code class="overflow-x-auto rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-slate-200">{{ $integrations['parcel2go']['callback_url'] }}</code>
|
||||||
<p class="text-slate-400">Save its Client ID above, then sign in and approve <code>my-profile</code> access. The account tokens are encrypted in the database. If Parcel2Go does not issue a refresh token, reconnect when access expires. If consent rejects this scope, contact <a class="text-brand hover:underline" href="mailto:apihelp@parcel2go.com">apihelp@parcel2go.com</a>.</p>
|
<p>Save its Client ID under Advanced API settings, then connect below. If consent is rejected, contact <a class="text-brand underline" href="mailto:apihelp@parcel2go.com">Parcel2Go API support</a>.</p>
|
||||||
@if($integrations['parcel2go']['profile_client_configured'])<form method="post" action="{{ route('parcel2go.connect') }}">@csrf<button class="rounded-xl border border-brand/40 px-4 py-2 font-semibold text-brand hover:bg-brand/10">{{ $integrations['parcel2go']['profile_connected'] ? 'Reconnect Parcel2Go account' : 'Connect Parcel2Go account' }}</button></form>@endif
|
@if($integrations['parcel2go']['profile_client_configured'])<form method="post" action="{{ route('parcel2go.connect') }}">@csrf<button class="rounded-xl border border-brand/40 px-4 py-2 font-semibold text-brand hover:bg-brand/10">{{ $integrations['parcel2go']['profile_connected'] ? 'Reconnect account' : 'Connect account' }}</button></form>@endif
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
|
<section id="bambuddy" class="scroll-mt-6 rounded-2xl border border-slate-800 bg-slate-900 p-6">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div><h3 class="text-lg font-semibold">BamBuddy</h3><p class="mt-1 text-sm text-slate-400">Live print farm data.</p></div>
|
<div><h3 class="text-lg font-semibold">BamBuddy</h3><p class="mt-1 text-sm text-slate-400">Live print farm data.</p></div>
|
||||||
<span @class(['rounded-full px-3 py-1 text-xs font-semibold', 'bg-emerald-500/10 text-emerald-300' => $integrations['bambuddy']['configured'], 'bg-amber-500/10 text-amber-200' => ! $integrations['bambuddy']['configured']])>{{ $integrations['bambuddy']['configured'] ? 'Configured' : 'Setup needed' }}</span>
|
<span @class(['rounded-full px-3 py-1 text-xs font-semibold', 'bg-emerald-500/10 text-emerald-300' => $integrations['bambuddy']['configured'], 'bg-amber-500/10 text-amber-200' => ! $integrations['bambuddy']['configured']])>{{ $integrations['bambuddy']['configured'] ? ($integrations['bambuddy']['stored'] ? 'Saved in dashboard' : 'Using server settings') : 'Setup needed' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-xs text-slate-500">{{ $integrations['bambuddy']['stored'] ? 'Saved in the database.' : 'Using server values where available. Save to import them.' }}</p>
|
|
||||||
<form method="post" action="{{ route('settings.integrations.update', 'bambuddy') }}" class="mt-5 grid gap-4">
|
<form method="post" action="{{ route('settings.integrations.update', 'bambuddy') }}" class="mt-5 grid gap-4">
|
||||||
@csrf @method('PUT')
|
@csrf @method('PUT')
|
||||||
<label class="grid gap-2 text-sm text-slate-300">BamBuddy URL<input name="url" type="url" maxlength="2048" value="{{ old('url', $integrations['bambuddy']['url']) }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
<input type="hidden" name="settings_section" value="bambuddy">
|
||||||
<label class="grid gap-2 text-sm text-slate-300">API key<input name="api_key" type="password" autocomplete="new-password" maxlength="512" placeholder="Leave blank to keep current value" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
<label class="grid gap-2 text-sm text-slate-300">BamBuddy URL<input name="url" type="url" maxlength="2048" value="{{ old('settings_section') === 'bambuddy' ? old('url', $integrations['bambuddy']['url']) : $integrations['bambuddy']['url'] }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
||||||
<button class="w-fit rounded-xl bg-brand px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Save BamBuddy</button>
|
<label class="grid gap-2 text-sm text-slate-300">API key<input name="api_key" type="password" autocomplete="new-password" maxlength="512" placeholder="{{ $integrations['bambuddy']['configured'] ? 'Saved — leave blank to keep' : 'Enter API key' }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
||||||
|
<button class="w-fit rounded-xl bg-brand px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Save BamBuddy settings</button>
|
||||||
</form>
|
</form>
|
||||||
|
<p class="mt-3 text-xs text-slate-500">Leave the API key blank to keep its current value.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
|
<section id="dryer" class="scroll-mt-6 rounded-2xl border border-slate-800 bg-slate-900 p-6">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div><h3 class="text-lg font-semibold">Filament dryer</h3><p class="mt-1 text-sm text-slate-400">Status, cycles and fault controls.</p></div>
|
<div><h3 class="text-lg font-semibold">Filament dryer</h3><p class="mt-1 text-sm text-slate-400">Status, cycles and fault controls.</p></div>
|
||||||
<span @class(['rounded-full px-3 py-1 text-xs font-semibold', 'bg-emerald-500/10 text-emerald-300' => $integrations['dryer']['configured'], 'bg-amber-500/10 text-amber-200' => ! $integrations['dryer']['configured']])>{{ $integrations['dryer']['configured'] ? 'Configured' : 'Setup needed' }}</span>
|
<span @class(['rounded-full px-3 py-1 text-xs font-semibold', 'bg-emerald-500/10 text-emerald-300' => $integrations['dryer']['configured'], 'bg-amber-500/10 text-amber-200' => ! $integrations['dryer']['configured']])>{{ $integrations['dryer']['configured'] ? ($integrations['dryer']['stored'] ? 'Saved in dashboard' : 'Using server settings') : 'Setup needed' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-xs text-slate-500">{{ $integrations['dryer']['stored'] ? 'Saved in the database.' : 'Using server values where available. Save to import them.' }}</p>
|
|
||||||
<form method="post" action="{{ route('settings.integrations.update', 'dryer') }}" class="mt-5 grid gap-4">
|
<form method="post" action="{{ route('settings.integrations.update', 'dryer') }}" class="mt-5 grid gap-4">
|
||||||
@csrf @method('PUT')
|
@csrf @method('PUT')
|
||||||
<label class="grid gap-2 text-sm text-slate-300">Dryer API URL<input name="url" type="url" maxlength="2048" value="{{ old('url', $integrations['dryer']['url']) }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
<input type="hidden" name="settings_section" value="dryer">
|
||||||
<label class="grid gap-2 text-sm text-slate-300">API key<input name="api_key" type="password" autocomplete="new-password" maxlength="512" placeholder="Leave blank to keep current value" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
<label class="grid gap-2 text-sm text-slate-300">Dryer API URL<input name="url" type="url" maxlength="2048" value="{{ old('settings_section') === 'dryer' ? old('url', $integrations['dryer']['url']) : $integrations['dryer']['url'] }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100"></label>
|
||||||
<button class="w-fit rounded-xl bg-brand px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Save dryer</button>
|
<label class="grid gap-2 text-sm text-slate-300">API key<input name="api_key" type="password" autocomplete="new-password" maxlength="512" placeholder="{{ $integrations['dryer']['configured'] ? 'Saved — leave blank to keep' : 'Enter API key' }}" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3 text-slate-100 placeholder:text-slate-500"></label>
|
||||||
|
<button class="w-fit rounded-xl bg-brand px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-brand/90">Save dryer settings</button>
|
||||||
</form>
|
</form>
|
||||||
|
<p class="mt-3 text-xs text-slate-500">Leave the API key blank to keep its current value.</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
|
|
||||||
<h3 class="text-lg font-semibold">OAuth callback</h3>
|
|
||||||
<p class="mt-1 text-sm text-slate-400">Register this exact HTTPS URL in your Etsy developer app.</p>
|
|
||||||
<code class="mt-4 block overflow-x-auto rounded-xl border border-slate-800 bg-slate-950 px-4 py-3 text-sm text-slate-200">{{ $redirectUri }}</code>
|
|
||||||
@if($configured)<a href="{{ route('etsy.index') }}" class="mt-5 inline-flex rounded-xl border border-slate-700 px-4 py-3 text-sm font-semibold text-slate-200 hover:border-brand hover:text-white">Go to Etsy shop</a>@endif
|
|
||||||
</section>
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class EtsySettingsTest extends TestCase
|
|||||||
$this->assertArrayNotHasKey('shared_secret', $credentials->toArray());
|
$this->assertArrayNotHasKey('shared_secret', $credentials->toArray());
|
||||||
|
|
||||||
$this->actingAs($admin)->get(route('settings.index'))
|
$this->actingAs($admin)->get(route('settings.index'))
|
||||||
->assertOk()->assertSee('Credentials saved in the database.')
|
->assertOk()->assertSee('App credentials saved in dashboard.')
|
||||||
->assertDontSee('saved-key')->assertDontSee('saved-secret');
|
->assertDontSee('saved-key')->assertDontSee('saved-secret');
|
||||||
$this->actingAs($admin)->post(route('etsy.connect'))
|
$this->actingAs($admin)->post(route('etsy.connect'))
|
||||||
->assertRedirectContains('client_id=saved-key');
|
->assertRedirectContains('client_id=saved-key');
|
||||||
|
|||||||
@@ -91,11 +91,16 @@ class IntegrationSettingsTest extends TestCase
|
|||||||
|
|
||||||
public function test_endpoint_urls_must_use_https(): void
|
public function test_endpoint_urls_must_use_https(): void
|
||||||
{
|
{
|
||||||
|
config()->set('services.bambuddy.url', 'https://bambuddy.example.test');
|
||||||
|
config()->set('services.dryer.url', 'https://dryer.example.test/api/v1');
|
||||||
$this->actingAs(User::factory()->create(['role' => 'admin']))
|
$this->actingAs(User::factory()->create(['role' => 'admin']))
|
||||||
->put(route('settings.integrations.update', 'dryer'), [
|
->put(route('settings.integrations.update', 'dryer'), [
|
||||||
'url' => 'http://dryer.example.test', 'api_key' => 'key',
|
'url' => 'http://dryer.example.test', 'api_key' => 'key', 'settings_section' => 'dryer',
|
||||||
])->assertSessionHasErrors('url');
|
])->assertSessionHasErrors('url');
|
||||||
$this->assertDatabaseCount('integration_settings', 0);
|
$this->assertDatabaseCount('integration_settings', 0);
|
||||||
|
$this->get(route('settings.index'))->assertOk()
|
||||||
|
->assertSee('value="https://bambuddy.example.test"', false)
|
||||||
|
->assertSee('value="http://dryer.example.test"', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_parcel2go_scope_is_validated_and_saved(): void
|
public function test_parcel2go_scope_is_validated_and_saved(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user