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
+14 -6
View File
@@ -8,9 +8,16 @@ use Throwable;
class Bambuddy
{
public function __construct(private IntegrationSettings $settings) {}
public function url(): string
{
return (string) ($this->settings->for('bambuddy')['url'] ?? '');
}
public function configured(): bool
{
return filled(config('services.bambuddy.url')) && filled(config('services.bambuddy.api_key'));
return filled($this->url()) && filled($this->settings->for('bambuddy')['api_key'] ?? null);
}
public function printers(): array
@@ -19,17 +26,18 @@ class Bambuddy
throw new RuntimeException('BamBuddy API key is not configured.');
}
$url = rtrim(config('services.bambuddy.url'), '/').'/api/v1/printers';
$url = rtrim($this->url(), '/').'/api/v1/printers';
$apiKey = $this->settings->for('bambuddy')['api_key'];
$printers = Http::withHeaders(['X-API-Key' => config('services.bambuddy.api_key')])
$printers = Http::withHeaders(['X-API-Key' => $apiKey])
->acceptJson()->connectTimeout(3)->timeout(8)->get($url)->throw()->json() ?? [];
return collect($printers)->filter(fn ($printer) => is_array($printer) && isset($printer['id']))
->take(20)->map(function (array $printer): array {
->take(20)->map(function (array $printer) use ($apiKey): array {
try {
$status = Http::withHeaders(['X-API-Key' => config('services.bambuddy.api_key')])
$status = Http::withHeaders(['X-API-Key' => $apiKey])
->acceptJson()->connectTimeout(3)->timeout(5)
->get(rtrim(config('services.bambuddy.url'), '/').'/api/v1/printers/'.(int) $printer['id'].'/status')
->get(rtrim($this->url(), '/').'/api/v1/printers/'.(int) $printer['id'].'/status')
->throw()->json() ?? [];
return $printer + ['live' => $status];
+26 -5
View File
@@ -7,10 +7,31 @@ use RuntimeException;
class DryerClient
{
public function __construct(private IntegrationSettings $settings) {}
public function configured(): bool
{
$settings = $this->settings->for('dryer');
return filled($settings['url'] ?? null) && filled($settings['api_key'] ?? null);
}
public function dashboardUrl(): string
{
$url = rtrim((string) ($this->settings->for('dryer')['url'] ?? ''), '/');
return preg_replace('#/api/v1$#', '', $url) ?? $url;
}
private function apiKey(): string
{
return (string) ($this->settings->for('dryer')['api_key'] ?? '');
}
private function url(string $path): string
{
$base = config('services.dryer.url');
if (! $base || ! config('services.dryer.api_key')) {
$base = $this->settings->for('dryer')['url'] ?? null;
if (! $this->configured()) {
throw new RuntimeException('Dryer URL and API key must be configured.');
}
@@ -24,13 +45,13 @@ class DryerClient
public function status(): array
{
return Http::withHeaders(['X-API-Key' => config('services.dryer.api_key')])->acceptJson()
return Http::withHeaders(['X-API-Key' => $this->apiKey()])->acceptJson()
->connectTimeout(3)->timeout(7)->get($this->url('status'))->throw()->json() ?? [];
}
public function history(): array
{
return Http::withHeaders(['X-API-Key' => config('services.dryer.api_key')])->acceptJson()
return Http::withHeaders(['X-API-Key' => $this->apiKey()])->acceptJson()
->connectTimeout(3)->timeout(7)->get($this->url('history'), ['limit' => 60])->throw()->json() ?? [];
}
@@ -53,7 +74,7 @@ class DryerClient
private function command(string $path, array $payload = []): array
{
return Http::withHeaders(['X-API-Key' => config('services.dryer.api_key')])->acceptJson()
return Http::withHeaders(['X-API-Key' => $this->apiKey()])->acceptJson()
->connectTimeout(3)->timeout(7)->post($this->url($path), [
'requestId' => 'dashboard-'.bin2hex(random_bytes(12)), ...$payload,
])->throw()->json() ?? [];
+6 -4
View File
@@ -11,7 +11,7 @@ class Etsy
{
private const API_URL = 'https://api.etsy.com/v3';
/** @var array{keystring: ?string, shared_secret: ?string}|null */
/** @var array{keystring: ?string, shared_secret: ?string, shop_name: string, redirect_uri: ?string}|null */
private ?array $credentials = null;
public function configured(): bool
@@ -51,7 +51,7 @@ class Etsy
if (empty($shop['shop_id'])) {
throw new RuntimeException('This Etsy account does not own a shop.');
}
if (strcasecmp($shop['shop_name'] ?? '', config('services.etsy.shop_name', 'HickLab3D')) !== 0) {
if (strcasecmp($shop['shop_name'] ?? '', $this->credentials()['shop_name']) !== 0) {
throw new RuntimeException('The Etsy account does not own the configured HickLab3D shop.');
}
@@ -108,10 +108,10 @@ class Etsy
public function redirectUri(): string
{
return config('services.etsy.redirect_uri') ?: route('etsy.callback');
return $this->credentials()['redirect_uri'] ?: route('etsy.callback');
}
/** @return array{keystring: ?string, shared_secret: ?string} */
/** @return array{keystring: ?string, shared_secret: ?string, shop_name: string, redirect_uri: ?string} */
private function credentials(): array
{
if ($this->credentials !== null) {
@@ -123,6 +123,8 @@ class Etsy
return $this->credentials = [
'keystring' => $stored?->keystring ?: config('services.etsy.keystring'),
'shared_secret' => $stored?->shared_secret ?: config('services.etsy.shared_secret'),
'shop_name' => $stored?->shop_name ?: config('services.etsy.shop_name', 'HickLab3D'),
'redirect_uri' => $stored?->redirect_uri ?: config('services.etsy.redirect_uri'),
];
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Services;
use App\Models\IntegrationSetting;
class IntegrationSettings
{
/** @var array<string, array<string, mixed>> */
private array $resolved = [];
/** @return array<string, mixed> */
public function for(string $provider): array
{
if (! isset($this->resolved[$provider])) {
$this->resolved[$provider] = array_replace(
config('services.'.$provider, []),
IntegrationSetting::find($provider)?->settings ?? [],
);
}
return $this->resolved[$provider];
}
}
+4 -2
View File
@@ -8,6 +8,8 @@ use RuntimeException;
class Parcel2Go
{
public function __construct(private IntegrationSettings $settings) {}
public function fetch(string $path): array
{
return Http::withToken($this->accessToken())->acceptJson()->connectTimeout(3)->timeout(15)
@@ -90,12 +92,12 @@ class Parcel2Go
private function url(string $path): string
{
return rtrim(config('services.parcel2go.base_url'), '/').'/'.ltrim($path, '/');
return rtrim($this->settings->for('parcel2go')['base_url'], '/').'/'.ltrim($path, '/');
}
private function accessToken(): string
{
$settings = config('services.parcel2go');
$settings = $this->settings->for('parcel2go');
if (! $settings['client_id'] || ! $settings['client_secret'] || ! $settings['auth_url']) {
throw new RuntimeException('Parcel2Go credentials are not configured.');
}