Move integration credentials into encrypted dashboard settings
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 1m42s

This commit is contained in:
Hickmeister
2026-10-11 16:15:02 +01:00
parent 96561dc3d2
commit ff9960fe06
23 changed files with 493 additions and 41 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
@extends('layouts.app')
@section('title', 'Filament dryer')
@section('content')
<div class="flex flex-wrap items-end justify-between gap-4"><div><p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Workshop tools</p><h2 class="mt-2 text-2xl font-bold">Filament dryer</h2><p class="mt-1 text-slate-400">Live programme, sensor readings and relay state.</p></div><a href="https://filamentdryer.hickmeister.uk" target="_blank" rel="noopener noreferrer" class="rounded-xl border border-brand/40 px-4 py-3 text-sm font-semibold text-brand hover:bg-brand/10">Open dryer ↗</a></div>
<div class="flex flex-wrap items-end justify-between gap-4"><div><p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Workshop tools</p><h2 class="mt-2 text-2xl font-bold">Filament dryer</h2><p class="mt-1 text-slate-400">Live programme, sensor readings and relay state.</p></div>@if($dryerUrl)<a href="{{ $dryerUrl }}" target="_blank" rel="noopener noreferrer" class="rounded-xl border border-brand/40 px-4 py-3 text-sm font-semibold text-brand hover:bg-brand/10">Open dryer ↗</a>@endif</div>
<p id="dryer-message" role="status" class="rounded-xl border border-slate-700 bg-slate-900 px-4 py-3 text-sm text-slate-400">Connecting to dryer…</p>
<div id="dryer-fault" role="alert" hidden class="rounded-xl border border-rose-500/40 bg-rose-500/10 p-4 text-sm text-rose-200"><strong>Dryer fault</strong><p id="fault-detail" class="mt-1"></p></div>
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">@foreach([['dryer-state','State'],['temperature','Temperature'],['humidity','Humidity'],['remaining','Remaining time']] as [$id,$label])<div class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><p class="text-sm text-slate-400">{{ $label }}</p><p id="{{ $id }}" class="mt-5 text-3xl font-bold">—</p></div>@endforeach</div>
+1 -1
View File
@@ -1,7 +1,7 @@
@extends('layouts.app')
@section('title', 'Print farm')
@section('content')
<div class="flex flex-wrap items-end justify-between gap-4"><div><p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Production</p><h2 class="mt-2 text-2xl font-bold">BamBuddy printers</h2><p class="mt-1 text-slate-400">Live state, progress and temperatures from the workshop.</p></div><a href="{{ config('services.bambuddy.url') }}" target="_blank" rel="noopener noreferrer" class="rounded-xl border border-brand/40 px-4 py-3 text-sm font-semibold text-brand hover:bg-brand/10">Open BamBuddy ↗</a></div>
<div class="flex flex-wrap items-end justify-between gap-4"><div><p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Production</p><h2 class="mt-2 text-2xl font-bold">BamBuddy printers</h2><p class="mt-1 text-slate-400">Live state, progress and temperatures from the workshop.</p></div><a href="{{ $bambuddyUrl }}" target="_blank" rel="noopener noreferrer" class="rounded-xl border border-brand/40 px-4 py-3 text-sm font-semibold text-brand hover:bg-brand/10">Open BamBuddy ↗</a></div>
@if($integrationError)<div role="alert" class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-200">{{ $integrationError }}</div>@endif
<div class="grid gap-4 lg:grid-cols-2">@forelse($printers as $printer)
@php $live = $printer['live'] ?? []; $progress = min(100, max(0, (float) ($live['progress'] ?? 0))); $printerErrors = is_array($live['hms_errors'] ?? null) ? $live['hms_errors'] : []; @endphp
+60 -6
View File
@@ -3,14 +3,14 @@
@section('content')
<div>
<p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Integrations</p>
<h2 class="mt-2 text-2xl font-bold">Etsy app credentials</h2>
<p class="mt-1 text-sm text-slate-400">Save the credentials from your Etsy developer app, then connect your shop with OAuth.</p>
<h2 class="mt-2 text-2xl font-bold">Integration settings</h2>
<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>
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="text-lg font-semibold">API access</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>
</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>
@@ -21,19 +21,73 @@
@method('PUT')
<label class="grid gap-2 text-sm text-slate-300">
Etsy app keystring
<input name="keystring" type="password" autocomplete="new-password" maxlength="255" placeholder="{{ $storedCredentials ? 'Leave blank to keep saved 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" {{ $storedCredentials ? '' : 'required' }}>
<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' }}>
</label>
<label class="grid gap-2 text-sm text-slate-300">
Etsy shared secret
<input name="shared_secret" type="password" autocomplete="new-password" maxlength="255" placeholder="{{ $storedCredentials ? 'Leave blank to keep saved 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" {{ $storedCredentials ? '' : 'required' }}>
<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' }}>
</label>
<label class="grid gap-2 text-sm text-slate-300">
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">
</label>
<label class="grid gap-2 text-sm text-slate-300">
OAuth 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>
<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>
</div>
</form>
<p class="mt-4 text-xs text-slate-500">Saved values are encrypted and are never shown again. Changing the app key disconnects the current Etsy shop.</p>
<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>
</section>
<div class="grid gap-5 xl:grid-cols-2">
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
<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>
<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>
</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>
<form method="post" action="{{ route('settings.integrations.update', 'parcel2go') }}" class="mt-5 grid gap-4">
@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 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">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">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>
<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>
</section>
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
<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>
<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>
</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">
@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>
<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>
<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>
</form>
</section>
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6">
<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>
<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>
</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">
@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>
<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>
<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>
</form>
</section>
</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>