some work on the license manager
This commit is contained in:
@@ -109,7 +109,7 @@ protected function getApplicationKeyPair(): array
|
|||||||
* @param string $privateKey The private key to use for signing
|
* @param string $privateKey The private key to use for signing
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected function generateSignature($licenseData, $privateKey)
|
protected function sign($licenseData, $privateKey)
|
||||||
{
|
{
|
||||||
$licenseJson = json_encode($licenseData);
|
$licenseJson = json_encode($licenseData);
|
||||||
if ($licenseJson === false) throw new \Exception('Fehler beim JSON-Encode der Lizenzdaten: ' . json_last_error_msg());
|
if ($licenseJson === false) throw new \Exception('Fehler beim JSON-Encode der Lizenzdaten: ' . json_last_error_msg());
|
||||||
@@ -155,7 +155,7 @@ public function store(Request $request)
|
|||||||
'expiration_date' => $snakeCaseData['expiration_date'],
|
'expiration_date' => $snakeCaseData['expiration_date'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$signature = $this->generateSignature($licenseData, $keyPair['privateKey']);
|
$signature = $this->sign($licenseData, $keyPair['privateKey']);
|
||||||
|
|
||||||
$license = License::create([
|
$license = License::create([
|
||||||
'name' => $snakeCaseData['name'],
|
'name' => $snakeCaseData['name'],
|
||||||
@@ -170,51 +170,62 @@ public function store(Request $request)
|
|||||||
return ApiDataTransformer::snakeToCamel($license->toArray());
|
return ApiDataTransformer::snakeToCamel($license->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the license signature and expiration date.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request The HTTP request containing the license data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function validate(Request $request)
|
||||||
|
{
|
||||||
|
$validatedData = $request->validate([
|
||||||
|
'name' => 'required|string',
|
||||||
|
'email' => 'required|email',
|
||||||
|
'product' => 'required|string',
|
||||||
|
'version' => 'required|string',
|
||||||
|
'isPerpetual' => 'required|boolean',
|
||||||
|
'expirationDate' => 'nullable|date',
|
||||||
|
'signature' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->isValid(ApiDataTransformer::camelToSnake($validatedData));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the license signature and expiration date.
|
* Validate the license signature and expiration date.
|
||||||
*
|
*
|
||||||
* The validation logic should be portable to other languages.
|
* The validation logic should be portable to other languages.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request The HTTP request containing the license ID
|
* @param array $licenseData The license data to validate
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function validate(Request $request)
|
protected function isValid(array $licenseData)
|
||||||
{
|
{
|
||||||
// Get the license ID from the request
|
|
||||||
$licenseId = $request->input('license_id');
|
|
||||||
|
|
||||||
// Find the license
|
|
||||||
$license = License::findOrFail($licenseId);
|
|
||||||
|
|
||||||
$publicKey = Storage::disk('local')->get($this->publicKeyPath());
|
$publicKey = Storage::disk('local')->get($this->publicKeyPath());
|
||||||
|
|
||||||
$licenseData = [
|
|
||||||
'name' => $license->name,
|
|
||||||
'email' => $license->email,
|
|
||||||
'product' => $license->product,
|
|
||||||
'version' => $license->version,
|
|
||||||
'is_perpetual' => $license->is_perpetual,
|
|
||||||
'expiration_date' => $license->expiration_date,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Convert the license data to JSON
|
// Convert the license data to JSON
|
||||||
$licenseJson = json_encode($licenseData);
|
$licenseJson = json_encode($licenseData);
|
||||||
|
|
||||||
|
|
||||||
// Verify the signature
|
// Verify the signature
|
||||||
$signature = base64_decode($license->signature);
|
$signature = base64_decode($licenseData['signature']);
|
||||||
$isSignatureValid = openssl_verify($licenseJson, $signature, $publicKey, OPENSSL_ALGO_SHA256);
|
$isSignatureValid = openssl_verify($licenseJson, $signature, $publicKey, OPENSSL_ALGO_SHA256);
|
||||||
|
|
||||||
// Check the expiration date if the license is not perpetual
|
// Check the expiration date if the license is not perpetual
|
||||||
$isExpired = false;
|
$isExpired = false;
|
||||||
if (!$license->is_perpetual && $license->expiration_date) {
|
if (!$licenseData['is_perpetual'] && $licenseData['expiration_date']) {
|
||||||
$isExpired = now()->gt($license->expiration_date);
|
$isExpired = now()->gt($licenseData['expiration_date']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the validation result
|
// Return the validation info
|
||||||
return [
|
return [
|
||||||
'is_valid' => $isSignatureValid === 1 && !$isExpired,
|
'isValid' => $isSignatureValid === 1 && !$isExpired,
|
||||||
'is_signature_valid' => $isSignatureValid === 1,
|
'isSignatureValid' => $isSignatureValid === 1,
|
||||||
'is_expired' => $isExpired,
|
'isExpired' => $isExpired,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +257,7 @@ public function download(int $id)
|
|||||||
// Create a response with the JSON file
|
// Create a response with the JSON file
|
||||||
$response = response($licenseJson, 200, [
|
$response = response($licenseJson, 200, [
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'Content-Disposition' => 'attachment; filename="' . $licenseData['product'] . '.json"',
|
'Content-Disposition' => 'attachment; filename="' . $licenseData['product'] . '.license"',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
@@ -296,7 +307,7 @@ public function update(Request $request, int $id)
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Generate the signature
|
// Generate the signature
|
||||||
$signature = $this->generateSignature($licenseData, $privateKey);
|
$signature = $this->sign($licenseData, $privateKey);
|
||||||
|
|
||||||
// Update the license
|
// Update the license
|
||||||
$license->update([
|
$license->update([
|
||||||
|
|||||||
Generated
+543
-523
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -51,7 +51,7 @@
|
|||||||
"vaul-vue": "^0.4.1",
|
"vaul-vue": "^0.4.1",
|
||||||
"vue": "^3.5.35",
|
"vue": "^3.5.35",
|
||||||
"vue-sonner": "^2.0.9",
|
"vue-sonner": "^2.0.9",
|
||||||
"vuedraggable": "^2.24.3"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.60.4",
|
"@rollup/rollup-linux-x64-gnu": "4.60.4",
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ onMounted(async () => {
|
|||||||
hotkey('mod+i', importLineItems, null, () => isOpen.value)
|
hotkey('mod+i', importLineItems, null, () => isOpen.value)
|
||||||
hotkey('mod+e', exportPdf, null, () => isOpen.value)
|
hotkey('mod+e', exportPdf, null, () => isOpen.value)
|
||||||
hotkey('mod+p', preview, null, () => isOpen.value)
|
hotkey('mod+p', preview, null, () => isOpen.value)
|
||||||
|
hotkey('mod+s', save)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Initial data from parent view
|
// Initial data from parent view
|
||||||
@@ -560,7 +561,7 @@ const handleFileUpload = async (event: Event) => {
|
|||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<!-- Save -->
|
<!-- Save -->
|
||||||
<Button v-if="invoice && isDirty" class="grow md:grow-0" size="sm" @click="save"
|
<Button class="grow md:grow-0" size="sm" @click="save"
|
||||||
:disabled="isSaving">
|
:disabled="isSaving">
|
||||||
<Loader2 v-if="isSaving" class="animate-spin" />
|
<Loader2 v-if="isSaving" class="animate-spin" />
|
||||||
<Check v-else />
|
<Check v-else />
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { toast } from 'vue-sonner'
|
|||||||
// Attributes, classes
|
// Attributes, classes
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// #region Clöasses
|
// #region Classes
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
@@ -202,6 +202,18 @@ export function randomDate(): Date {
|
|||||||
return date
|
return date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addDays(date: Date | string, days: number) {
|
||||||
|
const newDate = new Date(date);
|
||||||
|
newDate.setDate(newDate.getDate() + days);
|
||||||
|
return newDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addYears(date: Date | string, years: number) {
|
||||||
|
const newDate = new Date(date);
|
||||||
|
newDate.setFullYear(newDate.getFullYear() + years);
|
||||||
|
return newDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -228,11 +240,9 @@ Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming
|
|||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// Füge diese Typdefinitionen am Anfang der Datei hinzu
|
|
||||||
type HotkeyCallback = (params?: any) => void;
|
type HotkeyCallback = (params?: any) => void;
|
||||||
type HotkeyEnabledCallback = () => boolean;
|
type HotkeyEnabledCallback = () => boolean;
|
||||||
|
|
||||||
// Füge diese Variablen am Anfang der Datei hinzu
|
|
||||||
const registeredHotkeys = new Map<string, {
|
const registeredHotkeys = new Map<string, {
|
||||||
callback: HotkeyCallback;
|
callback: HotkeyCallback;
|
||||||
params: any;
|
params: any;
|
||||||
@@ -296,6 +306,15 @@ export function hotkey(
|
|||||||
.map(key => normalizeKeyName(key))
|
.map(key => normalizeKeyName(key))
|
||||||
.join('+')
|
.join('+')
|
||||||
|
|
||||||
|
// Warnung, wenn der registrierte Shortcut bekanntlich nicht zuverlässig
|
||||||
|
// vom Browser/OS unterdrückt werden kann.
|
||||||
|
if (UNPREVENTABLE_SHORTCUTS.has(normalizedCombination)) {
|
||||||
|
console.warn(
|
||||||
|
`Registriere Shortcut "${keyCombination}" (normalized: "${normalizedCombination}"). ` +
|
||||||
|
'Achtung: Das Standardverhalten dieses Shortcuts kann vom Browser/OS nicht zuverlässig verhindert werden.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Speichern der Hotkey-Informationen
|
// Speichern der Hotkey-Informationen
|
||||||
registeredHotkeys.set(normalizedCombination, {
|
registeredHotkeys.set(normalizedCombination, {
|
||||||
callback,
|
callback,
|
||||||
@@ -337,7 +356,11 @@ function normalizeKeyName(key: string): string {
|
|||||||
'arrowup': 'uparrow',
|
'arrowup': 'uparrow',
|
||||||
'arrowdown': 'downarrow',
|
'arrowdown': 'downarrow',
|
||||||
'return': 'enter',
|
'return': 'enter',
|
||||||
'esc': 'escape'
|
'esc': 'escape',
|
||||||
|
'opt': 'alt',
|
||||||
|
'option': 'alt',
|
||||||
|
'spacebar': 'space',
|
||||||
|
'space': 'space'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Konvertiere den Tastennamen in Kleinbuchstaben
|
// Konvertiere den Tastennamen in Kleinbuchstaben
|
||||||
@@ -347,6 +370,29 @@ function normalizeKeyName(key: string): string {
|
|||||||
return keyMap[lowerKey] || lowerKey
|
return keyMap[lowerKey] || lowerKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Liste bekannter Shortcuts, deren Standardverhalten unter vielen Browsern/OS
|
||||||
|
// nicht zuverlässig durch `preventDefault()` unterdrückt werden kann.
|
||||||
|
// Diese Liste ist nicht vollständig — sie dient nur zur Warnung beim Registrieren.
|
||||||
|
const UNPREVENTABLE_SHORTCUTS = new Set([
|
||||||
|
'cmd+tab',
|
||||||
|
'ctrl+tab',
|
||||||
|
'cmd+n',
|
||||||
|
'ctrl+n',
|
||||||
|
'cmd+space',
|
||||||
|
'ctrl+space',
|
||||||
|
'alt+tab',
|
||||||
|
'ctrl+alt+delete',
|
||||||
|
'f12',
|
||||||
|
'cmd+q',
|
||||||
|
'ctrl+q',
|
||||||
|
'cmd+w',
|
||||||
|
'ctrl+w',
|
||||||
|
'ctrl+shift+i',
|
||||||
|
'cmd+opt+i',
|
||||||
|
'ctrl+shift+j',
|
||||||
|
'cmd+opt+j'
|
||||||
|
]);
|
||||||
|
|
||||||
function handleKeyboardEvent(event: KeyboardEvent) {
|
function handleKeyboardEvent(event: KeyboardEvent) {
|
||||||
// Überprüfen, ob das Event-Element ein Eingabefeld, Textarea oder ähnliches ist
|
// Überprüfen, ob das Event-Element ein Eingabefeld, Textarea oder ähnliches ist
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
@@ -354,8 +400,9 @@ function handleKeyboardEvent(event: KeyboardEvent) {
|
|||||||
target.tagName === 'TEXTAREA' ||
|
target.tagName === 'TEXTAREA' ||
|
||||||
target.tagName === 'SELECT' ||
|
target.tagName === 'SELECT' ||
|
||||||
target.isContentEditable) {
|
target.isContentEditable) {
|
||||||
|
|
||||||
// Wenn das Event-Element ein Eingabefeld ist, den Shortcut nicht auslösen
|
// Wenn das Event-Element ein Eingabefeld ist, den Shortcut nicht auslösen
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erstellen einer normalisierten Tastaturkombination
|
// Erstellen einer normalisierten Tastaturkombination
|
||||||
@@ -390,7 +437,7 @@ function handleKeyboardEvent(event: KeyboardEvent) {
|
|||||||
// Verhindern der Standardaktion, falls gewünscht
|
// Verhindern der Standardaktion, falls gewünscht
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
return 0
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { License } from '@/types'
|
import { License } from '@/types'
|
||||||
import { toLocalDate } from '@/lib/utils'
|
import { newLicense } from '@/types/index.d'
|
||||||
|
import { toLocalDate, toShortISOString, hotkey } 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'
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
import AppHeader from '@/components/AppHeader.vue'
|
||||||
import { Head } from '@inertiajs/vue3'
|
import { Head } from '@inertiajs/vue3'
|
||||||
import { Table, TableRow, TableBody, TableCell, TableFooter, TableHead, TableHeader } from '@/components/ui/crm-table'
|
import { Table, TableRow, TableBody, TableCell, TableHead, TableHeader } from '@/components/ui/crm-table'
|
||||||
import { Input } from '@/components/ui/crm-input'
|
import { Input } from '@/components/ui/crm-input'
|
||||||
import { Badge } from '@/components/ui/crm-badge'
|
|
||||||
import Button from '@/components/ui/crm-button/Button.vue'
|
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 { Check, Download, Trash2, Plus, Delete, Search } from 'lucide-vue-next'
|
import { Download, Trash2, Plus, Delete, Search, CheckCircle, CheckCircle2, XCircle, CircleQuestionMark, BadgeQuestionMark, Eye } from 'lucide-vue-next'
|
||||||
import { alertStore } from "@/stores/alertStore"
|
import { alertStore } from "@/stores/alertStore"
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
|
|
||||||
@@ -29,6 +29,8 @@ const alert = alertStore()
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
licenses.value = props.licenseData
|
licenses.value = props.licenseData
|
||||||
|
searchField.value = document.getElementById('search')
|
||||||
|
hotkey('n', createLicense)
|
||||||
})
|
})
|
||||||
|
|
||||||
const fuse = computed(() => {
|
const fuse = computed(() => {
|
||||||
@@ -39,22 +41,36 @@ const fuse = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const filteredLicenses = computed(() => {
|
const filteredLicenses = computed(() => {
|
||||||
|
let filteredLicenses = licenses.value
|
||||||
let licenses = props.licenseData
|
|
||||||
|
|
||||||
// Filter by search query
|
// Filter by search query
|
||||||
if (searchQuery.value) {
|
if (searchQuery.value) {
|
||||||
licenses = fuse.value.search(searchQuery.value).map(result => result.item)
|
filteredLicenses = fuse.value.search(searchQuery.value).map(result => result.item)
|
||||||
}
|
}
|
||||||
|
return filteredLicenses
|
||||||
return licenses
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const downloadLicense = (id: number) => {
|
const createLicense = () => {
|
||||||
window.open('/licenses/download/' + id)
|
LicenseService.createLicense(newLicense()).then(license => {
|
||||||
|
if (license) licenses.value.unshift(license)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteLicense = (id: number) => {
|
const updateLicense = (license: License) => {
|
||||||
|
LicenseService.updateLicense(license).then((response) => {
|
||||||
|
if (response) license = response
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadLicense = (license: License) => {
|
||||||
|
window.open('/licenses/download/' + license.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteLicense = (license: License) => {
|
||||||
|
if (license.id == 0) {
|
||||||
|
licenses.value = licenses.value.filter(l => l != license)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
alert.show(
|
alert.show(
|
||||||
"Möchtest Du diese Lizenz wirklich löschen?",
|
"Möchtest Du diese Lizenz wirklich löschen?",
|
||||||
null,
|
null,
|
||||||
@@ -62,8 +78,8 @@ const deleteLicense = (id: number) => {
|
|||||||
actionText: "Löschen",
|
actionText: "Löschen",
|
||||||
actionVariant: "destructive",
|
actionVariant: "destructive",
|
||||||
onAction: async () => {
|
onAction: async () => {
|
||||||
LicenseService.deleteLicense(id).then(() => {
|
LicenseService.deleteLicense(license.id).then(() => {
|
||||||
licenses.value = licenses.value.filter(license => license.id != id)
|
licenses.value = licenses.value.filter(license => license.id != license.id)
|
||||||
}).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 })
|
||||||
})
|
})
|
||||||
@@ -72,6 +88,10 @@ const deleteLicense = (id: number) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const remToPx = (rem: number) => {
|
||||||
|
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -99,11 +119,12 @@ const deleteLicense = (id: number) => {
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template #right>
|
<template #right>
|
||||||
<!-- New button -->
|
<div class="flex gap-2 items-center">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
|
<!-- New button -->
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button @click="">
|
<Button @click="createLicense">
|
||||||
<Plus />
|
<Plus />
|
||||||
Neu
|
Neu
|
||||||
</Button>
|
</Button>
|
||||||
@@ -111,12 +132,12 @@ const deleteLicense = (id: number) => {
|
|||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<span>Neue Lizenz anlegen</span>
|
<span>Neue Lizenz anlegen</span>
|
||||||
<KbdGroup class="ml-2">
|
<KbdGroup class="ml-2">
|
||||||
<Kbd class="visible-mac">⌘ N</Kbd>
|
<Kbd>N</Kbd>
|
||||||
<Kbd class="visible-pc">Ctrl N</Kbd>
|
|
||||||
</KbdGroup>
|
</KbdGroup>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
|
|
||||||
@@ -124,10 +145,12 @@ const deleteLicense = (id: number) => {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead class="w-1/100 text-center">Gültig bis</TableHead>
|
<TableHead class="w-1/100 text-center">Gültig</TableHead>
|
||||||
<TableHead>Produkt</TableHead>
|
<TableHead class="w-1/100">Perm.</TableHead>
|
||||||
<TableHead>Version</TableHead>
|
<TableHead class="pl-7">Ablaufdatum</TableHead>
|
||||||
<TableHead colspan="2">Lizenznehmer</TableHead>
|
<TableHead class="pl-7">Produkt</TableHead>
|
||||||
|
<TableHead class="pl-7">Version</TableHead>
|
||||||
|
<TableHead class="pl-7" colspan="2">Lizenznehmer</TableHead>
|
||||||
<TableHead class="w-1/100 print:hidden"></TableHead>
|
<TableHead class="w-1/100 print:hidden"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -136,39 +159,45 @@ const deleteLicense = (id: 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">
|
||||||
<!-- <Check class="text-success inline-block" /> -->
|
<CircleQuestionMark class="inline-block text-muted-foreground" :size="remToPx(1)"
|
||||||
<Badge v-if="license.isPerpetual" variant="secondary">
|
v-if="license.validationInfo === undefined" />
|
||||||
Permanent
|
<CheckCircle2 class="inline-block text-success0" :size="remToPx(1)"
|
||||||
</Badge>
|
v-else-if="license.validationInfo.isExpired === false" />
|
||||||
<Badge v-else-if="Math.random() > 0.5" variant="success">
|
<XCircle class="inline-block text-destructive0" :size="remToPx(1)" v-else />
|
||||||
{{ toLocalDate(license.expirationDate || '') }}
|
</TableCell>
|
||||||
</Badge>
|
<TableCell class="text-center">
|
||||||
<Badge v-else variant="destructive">
|
<input type="checkbox" v-model="license.isPerpetual"
|
||||||
{{ toLocalDate(license.expirationDate || '') }}
|
v-on:change="updateLicense(license)" />
|
||||||
</Badge>
|
</TableCell>
|
||||||
|
<TableCell class="whitespace-nowrap">
|
||||||
|
<div class="text-center" v-if="license.isPerpetual">–</div>
|
||||||
|
<Input v-else type="date" :modelValue="toShortISOString(license.expirationDate || new Date())"
|
||||||
|
v-on:blur="updateLicense(license)" placeholder="Datum" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{{ license.product }}
|
<Input type="text" :modelValue="license.product" placeholder="Produkt Name"
|
||||||
|
v-on:blur="updateLicense(license)" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{{ license.version }}
|
<Input type="text" :modelValue="license.version" placeholder="1.0.0"
|
||||||
|
v-on:blur="updateLicense(license)" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{{ license.name }}<br />
|
<Input type="text" v-model="license.name" placeholder="Firma"
|
||||||
|
v-on:blur="updateLicense(license)" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<a :href="'mailto:' + license.email + '?subject=Ihre ' + license.product + '-Lizenz'"
|
<Input type="email" :modelValue="license.email" placeholder="E-Mail"
|
||||||
class="text-muted-foreground text-sm">{{ license.email
|
v-on:blur="updateLicense(license)" />
|
||||||
}}</a>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell class="print:hidden">
|
<TableCell class="print:hidden">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button size="icon" variant="ghost" @click="downloadLicense(license.id)">
|
<Button class="size-7" variant="ghost" @click="downloadLicense(license)">
|
||||||
<Download class="text-muted-foreground" />
|
<Download class="text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="icon" variant="ghost" @click="deleteLicense(license.id)">
|
<Button class="size-7" variant="ghost" @click="deleteLicense(license)">
|
||||||
<Trash2 class="text-muted-foreground" />
|
<Trash2 class="text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|||||||
@@ -113,9 +113,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(licenseId: number): Promise<{ is_valid: boolean, is_signature_valid: boolean, is_expired: boolean } | null> {
|
async validateLicense(license: License): Promise<{ isValid: boolean, isSignatureValid: boolean, isExpired: boolean } | null> {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/licenses/validate', { license_id: licenseId });
|
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 });
|
||||||
|
|||||||
Vendored
+9
-6
@@ -1,6 +1,6 @@
|
|||||||
import { InertiaLinkProps } from '@inertiajs/vue3';
|
import { InertiaLinkProps } from '@inertiajs/vue3';
|
||||||
import type { LucideIcon } from 'lucide-vue-next';
|
import type { LucideIcon } from 'lucide-vue-next';
|
||||||
import { calcDueDate } from '@/lib/utils';
|
import { calcDueDate, addYears } from '@/lib/utils';
|
||||||
|
|
||||||
export interface Auth {
|
export interface Auth {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -313,8 +313,13 @@ export interface License {
|
|||||||
isPerpetual: boolean;
|
isPerpetual: boolean;
|
||||||
expirationDate: string | null;
|
expirationDate: string | null;
|
||||||
signature: string;
|
signature: string;
|
||||||
createdAt: string;
|
validationInfo?: LicenseValidationInfo;
|
||||||
updatedAt: string;
|
}
|
||||||
|
|
||||||
|
export interface LicenseValidationInfo {
|
||||||
|
isValid: boolean;
|
||||||
|
isSignatureValid: boolean;
|
||||||
|
isExpired: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function newLicense(): License {
|
export function newLicense(): License {
|
||||||
@@ -325,10 +330,8 @@ export function newLicense(): License {
|
|||||||
product: '',
|
product: '',
|
||||||
version: '',
|
version: '',
|
||||||
isPerpetual: false,
|
isPerpetual: false,
|
||||||
expirationDate: null,
|
expirationDate: addYears(new Date(), 2),
|
||||||
signature: '',
|
signature: '',
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
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/{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::get('/lineitems/{invoiceId}', [LineItemController::class, 'index']);
|
Route::get('/lineitems/{invoiceId}', [LineItemController::class, 'index']);
|
||||||
Route::post('/lineitems/import', [LineItemController::class, 'importFromCsv']);
|
Route::post('/lineitems/import', [LineItemController::class, 'importFromCsv']);
|
||||||
|
|||||||
Reference in New Issue
Block a user