[License module] Fix casing in licenses, allow multiple uploads properly handle dirty states in frontend

This commit is contained in:
2026-07-23 14:24:26 +02:00
parent 35853171cb
commit d38ab2c2fc
5 changed files with 254 additions and 126 deletions
+144 -72
View File
@@ -21,7 +21,13 @@ class LicenseController extends Controller
public function index() public function index()
{ {
$licenses = License::orderBy('expirationDate', 'asc')->get(); $licenses = License::orderBy('expirationDate', 'asc')->get();
return ApiDataTransformer::snakeToCamel($licenses->toArray());
return array_map(function (License $license) {
$licenseData = ApiDataTransformer::snakeToCamel($license->toArray());
$licenseData['validationInfo'] = $this->buildValidationInfo($licenseData);
return $licenseData;
}, $licenses->all());
} }
/** /**
@@ -35,7 +41,10 @@ public function index()
public function single($id) public function single($id)
{ {
$license = License::findOrFail($id); $license = License::findOrFail($id);
return ApiDataTransformer::snakeToCamel($license->toArray()); $licenseData = ApiDataTransformer::snakeToCamel($license->toArray());
$licenseData['validationInfo'] = $this->buildValidationInfo($licenseData);
return $licenseData;
} }
/** /**
@@ -103,7 +112,8 @@ protected function getApplicationKeyPair(): array
/** /**
* Generate a signature for the license data. * Generate a signature for the license data.
* *
* Uses the private key to sign the data. * The payload is signed in camelCase because the signature is consumed by PHP-side
* validation logic and is not persisted as a database column shape.
* *
* @param array $licenseData The license data to sign * @param array $licenseData The license data to sign
* @param string $privateKey The private key to use for signing * @param string $privateKey The private key to use for signing
@@ -130,7 +140,7 @@ protected function sign($licenseData, $privateKey)
*/ */
public function store(Request $request) public function store(Request $request)
{ {
// Validate the request data // Validate the request data coming from the frontend in camelCase.
$validatedData = $request->validate([ $validatedData = $request->validate([
'name' => 'required|string', 'name' => 'required|string',
'email' => 'required|email', 'email' => 'required|email',
@@ -140,22 +150,24 @@ public function store(Request $request)
'expirationDate' => 'nullable|date', 'expirationDate' => 'nullable|date',
]); ]);
// Convert camelCase to snake_case
$snakeCaseData = ApiDataTransformer::camelToSnake($validatedData);
$keyPair = $this->getApplicationKeyPair(); $keyPair = $this->getApplicationKeyPair();
$licenseData = [ // Sign the camelCase business payload. The signature is validated by PHP code,
'name' => $snakeCaseData['name'], // not persisted as part of the database column schema.
'email' => $snakeCaseData['email'], $signatureData = [
'product' => $snakeCaseData['product'], 'name' => $validatedData['name'],
'version' => $snakeCaseData['version'], 'email' => $validatedData['email'],
'is_perpetual' => $snakeCaseData['is_perpetual'], 'product' => $validatedData['product'],
'expiration_date' => $snakeCaseData['expiration_date'], 'version' => $validatedData['version'],
'isPerpetual' => $validatedData['isPerpetual'],
'expirationDate' => $validatedData['isPerpetual'] ? null : $validatedData['expirationDate'],
]; ];
$signature = $this->sign($licenseData, $keyPair['privateKey']); $signature = $this->sign($signatureData, $keyPair['privateKey']);
// Convert incoming camelCase fields to snake_case only for database storage.
$snakeCaseData = ApiDataTransformer::camelToSnake($validatedData);
$license = License::create([ $license = License::create([
'name' => $snakeCaseData['name'], 'name' => $snakeCaseData['name'],
@@ -179,6 +191,8 @@ public function store(Request $request)
*/ */
public function validate(Request $request) public function validate(Request $request)
{ {
// The frontend sends camelCase, and the signature is verified against the same
// camelCase representation. We keep snake_case only for database persistence.
$validatedData = $request->validate([ $validatedData = $request->validate([
'name' => 'required|string', 'name' => 'required|string',
'email' => 'required|email', 'email' => 'required|email',
@@ -189,42 +203,45 @@ public function validate(Request $request)
'signature' => 'required|string', 'signature' => 'required|string',
]); ]);
return $this->isValid(ApiDataTransformer::camelToSnake($validatedData)); return $this->buildValidationInfo($validatedData);
} }
/** /**
* Validate the license signature and expiration date. * Build the validation info for a license payload.
* *
* The validation logic should be portable to other languages. * A license is considered valid when:
* - the signature is valid, and
* - the license is perpetual, or the expiration date is still in the future.
* *
* @param array $licenseData The license data to validate * @param array $licenseData The license data to validate
* @return array * @return array
*/ */
protected function isValid(array $licenseData) protected function buildValidationInfo(array $licenseData): array
{ {
$publicKey = Storage::disk('local')->get($this->publicKeyPath()); $publicKey = Storage::disk('local')->get($this->publicKeyPath());
$licenseData = ApiDataTransformer::snakeToCamel($licenseData);
$isPerpetual = (bool) ($licenseData['isPerpetual'] ?? false);
$expirationDate = $licenseData['expirationDate'] ?? null;
// Convert the license data to JSON // Validate against the same camelCase payload shape used to generate the signature.
$licenseJson = json_encode($licenseData); $licenseJson = json_encode([
'name' => $licenseData['name'] ?? '',
'email' => $licenseData['email'] ?? '',
'product' => $licenseData['product'] ?? '',
'version' => $licenseData['version'] ?? '',
'isPerpetual' => $isPerpetual,
'expirationDate' => $expirationDate,
]);
$signature = base64_decode($licenseData['signature'] ?? '', true);
$isSignatureValid = $signature !== false && openssl_verify($licenseJson, $signature, $publicKey, OPENSSL_ALGO_SHA256) === 1;
$isExpired = !$isPerpetual && !empty($expirationDate) && now()->gt($expirationDate);
// Verify the signature
$signature = base64_decode($licenseData['signature']);
$isSignatureValid = openssl_verify($licenseJson, $signature, $publicKey, OPENSSL_ALGO_SHA256);
// Check the expiration date if the license is not perpetual
$isExpired = false;
if (!$licenseData['is_perpetual'] && $licenseData['expiration_date']) {
$isExpired = now()->gt($licenseData['expiration_date']);
}
// Return the validation info
return [ return [
'isValid' => $isSignatureValid === 1 && !$isExpired, 'isValid' => $isSignatureValid && !$isExpired,
'isSignatureValid' => $isSignatureValid === 1, 'isSignatureValid' => $isSignatureValid,
'isExpired' => $isExpired, 'isExpired' => $isExpired,
]; ];
} }
@@ -247,12 +264,12 @@ public function download(int $id)
'product' => $license->product, 'product' => $license->product,
'version' => $license->version, 'version' => $license->version,
'is_perpetual' => $license->is_perpetual, 'is_perpetual' => $license->is_perpetual,
'expiration_date' => $license->expiration_date, 'expiration_date' => $license->expiration_date?->format('Y-m-d'),
'signature' => $license->signature, 'signature' => $license->signature,
]; ];
// Convert the license data to JSON // Normalize the outgoing payload to camelCase for the frontend consumer.
$licenseJson = json_encode($licenseData, JSON_PRETTY_PRINT); $licenseJson = json_encode(ApiDataTransformer::snakeToCamel($licenseData), JSON_PRETTY_PRINT);
// Create a response with the JSON file // Create a response with the JSON file
$response = response($licenseJson, 200, [ $response = response($licenseJson, 200, [
@@ -263,6 +280,91 @@ public function download(int $id)
return $response; return $response;
} }
/**
* Persist a single license update using the shared business logic.
*
* @param array $data The validated license payload in camelCase
* @param int $id The license id
* @param string $privateKey The application private key used to sign the payload
* @return array
*/
protected function persistLicenseUpdate(array $data, int $id, string $privateKey): array
{
$data['expirationDate'] = $data['isPerpetual'] ? null : $data['expirationDate'];
$signatureData = [
'name' => $data['name'],
'email' => $data['email'],
'product' => $data['product'],
'version' => $data['version'],
'isPerpetual' => $data['isPerpetual'],
'expirationDate' => $data['expirationDate'],
];
$signature = $this->sign($signatureData, $privateKey);
$snakeCaseData = ApiDataTransformer::camelToSnake($data);
$license = License::findOrFail($id);
$license->update([
'name' => $snakeCaseData['name'],
'email' => $snakeCaseData['email'],
'product' => $snakeCaseData['product'],
'version' => $snakeCaseData['version'],
'is_perpetual' => $snakeCaseData['is_perpetual'],
'expiration_date' => $snakeCaseData['expiration_date'],
'signature' => $signature,
]);
$licenseData = ApiDataTransformer::snakeToCamel($license->fresh()->toArray());
$licenseData['validationInfo'] = $this->buildValidationInfo($licenseData);
return $licenseData;
}
/**
* Update multiple licenses in a single request.
*
* This keeps the database writes in one HTTP request and avoids SQLite lock contention
* that can happen when a page sends one update request per dirty license in parallel.
*
* @param \Illuminate\Http\Request $request The HTTP request containing multiple updated license records
* @return array|\Illuminate\Http\JsonResponse
*/
public function updateMany(Request $request)
{
$validatedData = $request->validate([
'*.id' => 'required|integer',
'*.name' => 'required|string',
'*.email' => 'required|email',
'*.product' => 'required|string',
'*.version' => 'required|string',
'*.isPerpetual' => 'required|boolean',
'*.expirationDate' => 'nullable|date',
]);
$privateKey = $this->getApplicationKeyPair()['privateKey'];
DB::beginTransaction();
try {
$updatedLicenses = [];
foreach ($validatedData as $licensePayload) {
$updatedLicenses[] = $this->persistLicenseUpdate($licensePayload, $licensePayload['id'], $privateKey);
}
DB::commit();
return $updatedLicenses;
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'message' => 'Lizenzen konnten nicht aktualisiert werden',
'error' => $e->getMessage()
], 500);
}
}
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
* *
@@ -274,7 +376,7 @@ public function download(int $id)
*/ */
public function update(Request $request, int $id) public function update(Request $request, int $id)
{ {
// Validate the request data // Validate the request data coming from the frontend in camelCase.
$validatedData = $request->validate([ $validatedData = $request->validate([
'name' => 'required|string', 'name' => 'required|string',
'email' => 'required|email', 'email' => 'required|email',
@@ -284,46 +386,16 @@ public function update(Request $request, int $id)
'expirationDate' => 'nullable|date', 'expirationDate' => 'nullable|date',
]); ]);
// Convert camelCase to snake_case
$snakeCaseData = ApiDataTransformer::camelToSnake($validatedData);
DB::beginTransaction(); DB::beginTransaction();
try { try {
// Find license
$license = License::findOrFail($id);
// Retrieve the private key for the license
$privateKey = $this->getApplicationKeyPair()['privateKey']; $privateKey = $this->getApplicationKeyPair()['privateKey'];
$licenseData = $this->persistLicenseUpdate($validatedData, $id, $privateKey);
// Generate the license data
$licenseData = [
'name' => $snakeCaseData['name'],
'email' => $snakeCaseData['email'],
'product' => $snakeCaseData['product'],
'version' => $snakeCaseData['version'],
'is_perpetual' => $snakeCaseData['is_perpetual'],
'expiration_date' => $snakeCaseData['expiration_date'],
];
// Generate the signature
$signature = $this->sign($licenseData, $privateKey);
// Update the license
$license->update([
'name' => $snakeCaseData['name'],
'email' => $snakeCaseData['email'],
'product' => $snakeCaseData['product'],
'version' => $snakeCaseData['version'],
'is_perpetual' => $snakeCaseData['is_perpetual'],
'expiration_date' => $snakeCaseData['expiration_date'],
'signature' => $signature,
]);
DB::commit(); DB::commit();
// Return the updated license // Return the updated license with its validation info attached.
return ApiDataTransformer::snakeToCamel($license->toArray()); return $licenseData;
} catch (\Exception $e) { } catch (\Exception $e) {
DB::rollBack(); DB::rollBack();
return response()->json([ return response()->json([
@@ -18,7 +18,7 @@ public function up(): void
$table->string('product'); $table->string('product');
$table->string('version'); $table->string('version');
$table->boolean('is_perpetual'); $table->boolean('is_perpetual');
$table->string('expiration_date')->nullable();; $table->string('expiration_date')->nullable();
$table->string('signature'); $table->string('signature');
$table->timestamps(); $table->timestamps();
}); });
+85 -46
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { License } from '@/types' import { License } from '@/types'
import { newLicense } from '@/types/index.d' import { newLicense } from '@/types/index.d'
import { toLocalDate, toShortISOString, hotkey } from '@/lib/utils' import { hotkey, toShortISOString } from '@/lib/utils'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { onMounted, ref, computed } from 'vue' import { onMounted, ref, computed } from 'vue'
import AppLayout from '@/layouts/AppLayout.vue' import AppLayout from '@/layouts/AppLayout.vue'
@@ -13,85 +13,126 @@ import Button from '@/components/ui/crm-button/Button.vue'
import { ButtonGroup } from '@/components/ui/button-group' import { ButtonGroup } from '@/components/ui/button-group'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Kbd, KbdGroup } from '@/components/ui/kbd' import { Kbd, KbdGroup } from '@/components/ui/kbd'
import LicenseService from '@/services/LicenseService'; import LicenseService from '@/services/LicenseService'
import { Download, Trash2, Plus, Delete, Search, CheckCircle, CheckCircle2, XCircle, CircleQuestionMark, BadgeQuestionMark, Eye } from 'lucide-vue-next' import { Download, Trash2, Plus, Delete, Search, CheckCircle2, XCircle, CircleQuestionMark, Save } from 'lucide-vue-next'
import { alertStore } from "@/stores/alertStore" import { alertStore } from '@/stores/alertStore'
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner'
interface Props { interface Props {
licenseData: License[] licenseData: License[]
} }
const props = defineProps<Props>() const props = defineProps<Props>()
// Working copy of the licenses shown in the table. All form inputs bind against this array.
const licenses = ref([] as License[]) const licenses = ref([] as License[])
// Snapshot of the initial server data used to detect whether a license has been changed.
const originalLicenses = ref([] as License[])
// Deep-clone the incoming license list so edits to the UI do not mutate the original props object.
const cloneLicenses = (licenseList: License[]) => JSON.parse(JSON.stringify(licenseList)) as License[]
const searchQuery = ref('') const searchQuery = ref('')
const searchField = ref() const searchField = ref()
const alert = alertStore() const alert = alertStore()
onMounted(() => { onMounted(() => {
licenses.value = props.licenseData licenses.value = cloneLicenses(props.licenseData)
originalLicenses.value = cloneLicenses(licenses.value)
searchField.value = document.getElementById('search') searchField.value = document.getElementById('search')
hotkey('n', createLicense) hotkey('n', createLicense)
}) })
const fuse = computed(() => { const fuse = computed(() => {
return new Fuse(props.licenseData, { return new Fuse(licenses.value, {
keys: ['name', 'email', 'product', 'version'], keys: ['name', 'email', 'product', 'version'],
threshold: 0.3 threshold: 0.3,
}) })
}) })
const filteredLicenses = computed(() => { const filteredLicenses = computed(() => {
let filteredLicenses = licenses.value let filtered = licenses.value
// Filter by search query
if (searchQuery.value) { if (searchQuery.value) {
filteredLicenses = fuse.value.search(searchQuery.value).map(result => result.item) filtered = fuse.value.search(searchQuery.value).map((result) => result.item)
} }
return filteredLicenses
return filtered
}) })
const isDirty = computed(() => {
return licenses.value.some(isLicenseDirty)
})
const isLicenseDirty = (license: License) => {
const originalLicense = originalLicenses.value.find((item) => item.id === license.id)
if (!originalLicense) {
return license.id === 0
}
return JSON.stringify(originalLicense) !== JSON.stringify(license)
}
const createLicense = () => { const createLicense = () => {
LicenseService.createLicense(newLicense()).then(license => { LicenseService.createLicense(newLicense()).then((license) => {
if (license) licenses.value.unshift(license) if (license) {
licenses.value.unshift(license)
originalLicenses.value = cloneLicenses(licenses.value)
}
}) })
} }
const updateLicense = (license: License) => { const updateLicenses = async () => {
LicenseService.updateLicense(license).then((response) => { const dirtyLicenses = licenses.value.filter(isLicenseDirty)
if (response) license = response
if (dirtyLicenses.length === 0) {
return
}
const response = await LicenseService.updateLicenses(dirtyLicenses)
if (response) {
response.forEach((updatedLicense) => {
var i = licenses.value.findIndex(license => license.id == updatedLicense.id)
licenses.value[i] = updatedLicense
}) })
} }
originalLicenses.value = cloneLicenses(licenses.value)
}
const downloadLicense = (license: License) => { const downloadLicense = (license: License) => {
window.open('/licenses/download/' + license.id) window.open('/licenses/download/' + license.id)
} }
const deleteLicense = (license: License) => { const deleteLicense = (license: License) => {
if (license.id == 0) { if (license.id === 0) {
licenses.value = licenses.value.filter(l => l != license) licenses.value = licenses.value.filter((item) => item !== license)
return return
} }
alert.show( alert.show(
"Möchtest Du diese Lizenz wirklich löschen?", 'Möchtest Du diese Lizenz wirklich löschen?',
null, null,
{ {
actionText: "Löschen", actionText: 'Löschen',
actionVariant: "destructive", actionVariant: 'destructive',
onAction: async () => { onAction: async () => {
LicenseService.deleteLicense(license.id).then(() => { LicenseService.deleteLicense(license.id).then(() => {
licenses.value = licenses.value.filter(license => license.id != license.id) licenses.value = licenses.value.filter((item) => item.id !== license.id)
originalLicenses.value = cloneLicenses(licenses.value)
}).catch((error) => { }).catch((error) => {
toast.error('Fehler beim Löschen der Lizenz', { duration: 5000, description: error.message }) toast.error('Fehler beim Löschen der Lizenz', { duration: 5000, description: error.message })
}) })
} },
} },
) )
} }
const remToPx = (rem: number) => { const remToPx = (rem: number) => {
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); return rem * parseFloat(getComputedStyle(document.documentElement).fontSize)
} }
</script> </script>
<template> <template>
@@ -121,19 +162,21 @@ const remToPx = (rem: number) => {
<template #right> <template #right>
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<TooltipProvider> <TooltipProvider>
<!-- Save button -->
<Button variant="action" @click="updateLicenses" v-if="isDirty">
<Save /> Speichern
</Button>
<!-- New button --> <!-- New button -->
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<Button @click="createLicense"> <Button @click="createLicense">
<Plus /> <Plus /> Neu
Neu
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<span>Neue Lizenz anlegen</span> <span>Neue Lizenz anlegen</span>
<KbdGroup class="ml-2"> <KbdGroup class="ml-2"><Kbd>N</Kbd></KbdGroup>
<Kbd>N</Kbd>
</KbdGroup>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@@ -159,37 +202,33 @@ const remToPx = (rem: number) => {
<TableRow v-for="license in filteredLicenses" :key="license.id"> <TableRow v-for="license in filteredLicenses" :key="license.id">
<TableCell class="text-center"> <TableCell class="text-center">
<CircleQuestionMark class="inline-block text-muted-foreground" :size="remToPx(1)" <CircleQuestionMark class="size-5 inline-block text-muted-foreground" :size="remToPx(1)"
v-if="license.validationInfo === undefined" /> v-if="license.validationInfo === undefined" />
<CheckCircle2 class="inline-block text-success0" :size="remToPx(1)" <CheckCircle2 class="size-5 inline-block text-success" :size="remToPx(1)"
v-else-if="license.validationInfo.isExpired === false" /> v-else-if="license.validationInfo.isExpired === false" />
<XCircle class="inline-block text-destructive0" :size="remToPx(1)" v-else /> <XCircle class="size-5 inline-block text-destructive" :size="remToPx(1)" v-else />
</TableCell> </TableCell>
<TableCell class="text-center"> <TableCell class="text-center">
<input type="checkbox" v-model="license.isPerpetual" <input type="checkbox" v-model="license.isPerpetual" />
v-on:change="updateLicense(license)" />
</TableCell> </TableCell>
<TableCell class="whitespace-nowrap"> <TableCell class="whitespace-nowrap">
<div class="text-center" v-if="license.isPerpetual"></div> <div class="text-center" v-if="license.isPerpetual"></div>
<Input v-else type="date" :modelValue="toShortISOString(license.expirationDate || new Date())" <Input v-else type="date" :modelValue="toShortISOString(license.expirationDate || new Date())"
v-on:blur="updateLicense(license)" placeholder="Datum" /> @update:model-value="(value) => license.expirationDate = value as string"
placeholder="Datum" />
</TableCell> </TableCell>
<TableCell> <TableCell>
<Input type="text" :modelValue="license.product" placeholder="Produkt Name" <Input type="text" v-model="license.product" placeholder="Produkt Name" />
v-on:blur="updateLicense(license)" />
</TableCell> </TableCell>
<TableCell> <TableCell>
<Input type="text" :modelValue="license.version" placeholder="1.0.0" <Input type="text" v-model="license.version" placeholder="1.0.0" />
v-on:blur="updateLicense(license)" />
</TableCell> </TableCell>
<TableCell> <TableCell>
<Input type="text" v-model="license.name" placeholder="Firma" <Input type="text" v-model="license.name" placeholder="Firma" />
v-on:blur="updateLicense(license)" />
</TableCell> </TableCell>
<TableCell> <TableCell>
<Input type="email" :modelValue="license.email" placeholder="E-Mail" <Input type="email" v-model="license.email" placeholder="E-Mail" />
v-on:blur="updateLicense(license)" />
</TableCell> </TableCell>
<TableCell class="print:hidden"> <TableCell class="print:hidden">
+20 -4
View File
@@ -1,5 +1,5 @@
import axios, { AxiosError } from 'axios'; import axios, { AxiosError } from 'axios';
import { License } from '@/types'; import { License, LicenseValidationInfo } from '@/types';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
const API_URL = '/api/licenses'; const API_URL = '/api/licenses';
@@ -53,7 +53,7 @@ export default {
}, },
/** /**
* Updates an existing license * Updates one existing license
* @param license - The license data to update * @param license - The license data to update
* @returns Promise<License | null> * @returns Promise<License | null>
*/ */
@@ -68,6 +68,22 @@ export default {
} }
}, },
/**
* Updates multiple licenses in one request.
* @param licenses - The dirty license data to update
* @returns Promise<License[] | null>
*/
async updateLicenses(licenses: License[]): Promise<License[] | null> {
try {
const response = await axios.put(API_URL, licenses);
return response.data;
} catch (error) {
toast.error('Fehler beim Aktualisieren der Lizenzen', { description: (error as AxiosError).message });
console.error(error);
return null;
}
},
/** /**
* Deletes a license by ID * Deletes a license by ID
* @param id - The ID of the license to delete * @param id - The ID of the license to delete
@@ -113,9 +129,9 @@ export default {
* @param licenseId - The ID of the license to validate * @param licenseId - The ID of the license to validate
* @returns Promise<{ is_valid: boolean, is_signature_valid: boolean, is_expired: boolean } | null> * @returns Promise<{ is_valid: boolean, is_signature_valid: boolean, is_expired: boolean } | null>
*/ */
async validateLicense(license: License): Promise<{ isValid: boolean, isSignatureValid: boolean, isExpired: boolean } | null> { async validateLicense(license: License): Promise<LicenseValidationInfo | null> {
try { try {
const response = await axios.post('/api/licenses/validate', { license }); const response = await axios.post('/api/licenses/validate', license);
return response.data; return response.data;
} catch (error) { } catch (error) {
toast.error('Fehler beim Validieren der Lizenz', { description: (error as AxiosError).message }); toast.error('Fehler beim Validieren der Lizenz', { description: (error as AxiosError).message });
+1
View File
@@ -60,6 +60,7 @@
Route::get('/licenses/{id}', [\App\Http\Controllers\LicenseController::class, 'single']); Route::get('/licenses/{id}', [\App\Http\Controllers\LicenseController::class, 'single']);
Route::delete('/licenses/{id}', [\App\Http\Controllers\LicenseController::class, 'destroy']); Route::delete('/licenses/{id}', [\App\Http\Controllers\LicenseController::class, 'destroy']);
Route::post('/licenses', [\App\Http\Controllers\LicenseController::class, 'store']); Route::post('/licenses', [\App\Http\Controllers\LicenseController::class, 'store']);
Route::put('/licenses', [\App\Http\Controllers\LicenseController::class, 'updateMany']);
Route::put('/licenses/{id}', [\App\Http\Controllers\LicenseController::class, 'update']); Route::put('/licenses/{id}', [\App\Http\Controllers\LicenseController::class, 'update']);
Route::post('/licenses/validate', [\App\Http\Controllers\LicenseController::class, 'validate']); Route::post('/licenses/validate', [\App\Http\Controllers\LicenseController::class, 'validate']);