diff --git a/app/Console/Commands/GenerateLicenseKeysCommand.php b/app/Console/Commands/GenerateLicenseKeysCommand.php
new file mode 100644
index 0000000..059272b
--- /dev/null
+++ b/app/Console/Commands/GenerateLicenseKeysCommand.php
@@ -0,0 +1,55 @@
+exists($privateKeyPath);
+ $publicExists = $disk->exists($publicKeyPath);
+
+ if (($privateExists || $publicExists) && !$this->option('force')) {
+ $this->error('Key pair already exists. Use --force to overwrite.');
+ return 1;
+ }
+
+ $keyPair = $this->generateKeyPair();
+ $disk->put($privateKeyPath, $keyPair['privateKey']);
+ $disk->put($publicKeyPath, $keyPair['publicKey']);
+
+ $this->info('Generated application license key pair.');
+ $this->line('Private key: ' . $privateKeyPath);
+ $this->line('Public key: ' . $publicKeyPath);
+
+ return 0;
+ }
+
+ protected function generateKeyPair(): array
+ {
+ $privateKey = openssl_pkey_new([
+ 'private_key_bits' => 2048,
+ 'private_key_type' => OPENSSL_KEYTYPE_RSA,
+ ]);
+
+ openssl_pkey_export($privateKey, $privateKeyPem);
+ $publicKeyDetails = openssl_pkey_get_details($privateKey);
+
+ return [
+ 'privateKey' => $privateKeyPem,
+ 'publicKey' => $publicKeyDetails['key'],
+ ];
+ }
+}
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 1c0a981..dcc6f1d 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -15,6 +15,7 @@ class Kernel extends ConsoleKernel
protected $commands = [
\App\Console\Commands\CaldavSyncCommand::class,
\App\Console\Commands\CheckInvoiceDueDatesCommand::class,
+ \App\Console\Commands\GenerateLicenseKeysCommand::class,
];
/**
diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php
index 6a6d6f9..baddf93 100644
--- a/app/Http/Controllers/InvoiceController.php
+++ b/app/Http/Controllers/InvoiceController.php
@@ -541,7 +541,7 @@ public function store(Request $request)
}
}
- public function update(Request $request, $id)
+ public function update(Request $request, int $id)
{
// Validate input data (expecting camelCase)
$validatedData = $request->validate([
diff --git a/app/Http/Controllers/LicenseController.php b/app/Http/Controllers/LicenseController.php
new file mode 100644
index 0000000..bc73e9e
--- /dev/null
+++ b/app/Http/Controllers/LicenseController.php
@@ -0,0 +1,341 @@
+get();
+ return ApiDataTransformer::snakeToCamel($licenses->toArray());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * Retrieves a single license by its ID.
+ *
+ * @param int $id The ID of the license to retrieve
+ * @return array
+ */
+ public function single($id)
+ {
+ $license = License::findOrFail($id);
+ return ApiDataTransformer::snakeToCamel($license->toArray());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * Renders the Inertia view for licenses
+ *
+ * @return \Inertia\Response
+ */
+ public function show()
+ {
+ return Inertia::render(
+ 'Licenses',
+ ['licenseData' => $this->index()]
+ );
+ }
+
+ /**
+ * Get the directory path for license keys.
+ *
+ * @return string
+ */
+ protected function licenseKeyDirectory(): string
+ {
+ return 'license-keys';
+ }
+
+ /**
+ * Get the path for the private key file.
+ *
+ * @return string
+ */
+ protected function privateKeyPath(): string
+ {
+ return $this->licenseKeyDirectory() . '/private.pem';
+ }
+
+ /**
+ * Get the path for the public key file.
+ *
+ * @return string
+ */
+ protected function publicKeyPath(): string
+ {
+ return $this->licenseKeyDirectory() . '/public.pem';
+ }
+
+ /**
+ * Get the application's key pair.
+ *
+ * @return array
+ */
+ protected function getApplicationKeyPair(): array
+ {
+ if (!Storage::disk('local')->exists($this->privateKeyPath()) || !Storage::disk('local')->exists($this->publicKeyPath())) {
+ abort(500, 'Application license key pair is missing. Generate it with `php artisan license:generate-keys`.');
+ }
+
+ return [
+ 'privateKey' => Storage::disk('local')->get($this->privateKeyPath()),
+ 'publicKey' => Storage::disk('local')->get($this->publicKeyPath()),
+ ];
+ }
+
+ /**
+ * Generate a signature for the license data.
+ *
+ * Uses the private key to sign the data.
+ *
+ * @param array $licenseData The license data to sign
+ * @param string $privateKey The private key to use for signing
+ * @return string
+ */
+ protected function generateSignature($licenseData, $privateKey)
+ {
+ $licenseJson = json_encode($licenseData);
+ if ($licenseJson === false) throw new \Exception('Fehler beim JSON-Encode der Lizenzdaten: ' . json_last_error_msg());
+
+ $success = openssl_sign($licenseJson, $signature, $privateKey, OPENSSL_ALGO_SHA256);
+ if ($success === false) throw new \Exception('Fehler beim Signieren der Lizenzdaten: ' . openssl_error_string());
+
+ return base64_encode($signature);
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * Validates the request data, generates a signature, and creates a new license.
+ *
+ * @param \Illuminate\Http\Request $request The HTTP request containing license data
+ * @return array
+ */
+ public function store(Request $request)
+ {
+ // Validate the request data
+ $validatedData = $request->validate([
+ 'name' => 'required|string',
+ 'email' => 'required|email',
+ 'product' => 'required|string',
+ 'version' => 'required|string',
+ 'isPerpetual' => 'required|boolean',
+ 'expirationDate' => 'nullable|date',
+ ]);
+
+ // Convert camelCase to snake_case
+ $snakeCaseData = ApiDataTransformer::camelToSnake($validatedData);
+
+
+ $keyPair = $this->getApplicationKeyPair();
+
+ $licenseData = [
+ 'name' => $snakeCaseData['name'],
+ 'email' => $snakeCaseData['email'],
+ 'product' => $snakeCaseData['product'],
+ 'version' => $snakeCaseData['version'],
+ 'is_perpetual' => $snakeCaseData['is_perpetual'],
+ 'expiration_date' => $snakeCaseData['expiration_date'],
+ ];
+
+ $signature = $this->generateSignature($licenseData, $keyPair['privateKey']);
+
+ $license = License::create([
+ 'name' => $snakeCaseData['name'],
+ 'email' => $snakeCaseData['email'],
+ 'product' => $snakeCaseData['product'],
+ 'version' => $snakeCaseData['version'],
+ 'is_perpetual' => $snakeCaseData['is_perpetual'],
+ 'expiration_date' => $snakeCaseData['expiration_date'],
+ 'signature' => $signature,
+ ]);
+
+ return ApiDataTransformer::snakeToCamel($license->toArray());
+ }
+
+ /**
+ * Validate the license signature and expiration date.
+ *
+ * The validation logic should be portable to other languages.
+ *
+ * @param \Illuminate\Http\Request $request The HTTP request containing the license ID
+ * @return array
+ */
+ public function validate(Request $request)
+ {
+ // 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());
+
+ $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
+ $licenseJson = json_encode($licenseData);
+
+ // Verify the signature
+ $signature = base64_decode($license->signature);
+ $isSignatureValid = openssl_verify($licenseJson, $signature, $publicKey, OPENSSL_ALGO_SHA256);
+
+ // Check the expiration date if the license is not perpetual
+ $isExpired = false;
+ if (!$license->is_perpetual && $license->expiration_date) {
+ $isExpired = now()->gt($license->expiration_date);
+ }
+
+ // Return the validation result
+ return [
+ 'is_valid' => $isSignatureValid === 1 && !$isExpired,
+ 'is_signature_valid' => $isSignatureValid === 1,
+ 'is_expired' => $isExpired,
+ ];
+ }
+
+ /**
+ * Download the license as a JSON file.
+ *
+ * @param int $id The ID of the license to download
+ * @return \Symfony\Component\HttpFoundation\StreamedResponse
+ */
+ public function download(int $id)
+ {
+ // Find the license
+ $license = License::findOrFail($id);
+
+ // Prepare the license data for download
+ $licenseData = [
+ 'name' => $license->name,
+ 'email' => $license->email,
+ 'product' => $license->product,
+ 'version' => $license->version,
+ 'is_perpetual' => $license->is_perpetual,
+ 'expiration_date' => $license->expiration_date,
+ 'signature' => $license->signature,
+ ];
+
+ // Convert the license data to JSON
+ $licenseJson = json_encode($licenseData, JSON_PRETTY_PRINT);
+
+ // Create a response with the JSON file
+ $response = response($licenseJson, 200, [
+ 'Content-Type' => 'application/json',
+ 'Content-Disposition' => 'attachment; filename="' . $licenseData['product'] . '.json"',
+ ]);
+
+ return $response;
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * Validates the request data, generates a new signature, and updates the license.
+ *
+ * @param \Illuminate\Http\Request $request The HTTP request containing updated license data
+ * @param int $id The ID of the license to update
+ * @return array|\Illuminate\Http\JsonResponse
+ */
+ public function update(Request $request, int $id)
+ {
+ // Validate the request data
+ $validatedData = $request->validate([
+ 'name' => 'required|string',
+ 'email' => 'required|email',
+ 'product' => 'required|string',
+ 'version' => 'required|string',
+ 'isPerpetual' => 'required|boolean',
+ 'expirationDate' => 'nullable|date',
+ ]);
+
+ // Convert camelCase to snake_case
+ $snakeCaseData = ApiDataTransformer::camelToSnake($validatedData);
+
+ DB::beginTransaction();
+
+ try {
+ // Find license
+ $license = License::findOrFail($id);
+
+ // Retrieve the private key for the license
+ $privateKey = $this->getApplicationKeyPair()['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->generateSignature($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();
+
+ // Return the updated license
+ return ApiDataTransformer::snakeToCamel($license->toArray());
+ } catch (\Exception $e) {
+ DB::rollBack();
+ return response()->json([
+ 'message' => 'Lizenz konnte nicht aktualisiert werden',
+ 'error' => $e->getMessage()
+ ], 500);
+ }
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * Deletes the specified license from the database.
+ *
+ * @param int $id The ID of the license to delete
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function destroy(int $id)
+ {
+ $license = License::findOrFail($id);
+ $license->delete();
+ return response()->json([
+ 'message' => 'Lizenz erfolgreich gelöscht'
+ ]);
+ }
+}
diff --git a/app/Models/License.php b/app/Models/License.php
new file mode 100644
index 0000000..3e3d8de
--- /dev/null
+++ b/app/Models/License.php
@@ -0,0 +1,27 @@
+ */
+ use HasFactory;
+
+ protected $fillable = [
+ 'name',
+ 'email',
+ 'product',
+ 'version',
+ 'is_perpetual',
+ 'expiration_date',
+ 'signature'
+ ];
+
+ protected $casts = [
+ 'is_perpetual' => 'boolean',
+ 'expiration_date' => 'datetime'
+ ];
+}
diff --git a/database/factories/LicenseFactory.php b/database/factories/LicenseFactory.php
new file mode 100644
index 0000000..dd6608d
--- /dev/null
+++ b/database/factories/LicenseFactory.php
@@ -0,0 +1,24 @@
+ $this->faker->name(),
+ 'email' => $this->faker->safeEmail(),
+ 'product' => "Tooloop Guide",
+ 'version' => $this->faker->semver(),
+ 'is_perpetual' => rand(0, 10) > 7,
+ 'expiration_date' => $this->faker->dateTimeThisDecade(),
+ 'signature' => $this->faker->sha256()
+ ];
+ }
+}
diff --git a/database/migrations/2026_05_25_142250_create_licenses_table.php b/database/migrations/2026_05_25_142250_create_licenses_table.php
new file mode 100644
index 0000000..ed28ffa
--- /dev/null
+++ b/database/migrations/2026_05_25_142250_create_licenses_table.php
@@ -0,0 +1,34 @@
+id();
+ $table->string('name');
+ $table->string('email');
+ $table->string('product');
+ $table->string('version');
+ $table->boolean('is_perpetual');
+ $table->string('expiration_date')->nullable();;
+ $table->string('signature');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('licenses');
+ }
+};
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index b3a6fbe..30de5df 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -22,6 +22,7 @@ public function run(): void
SettingsTableSeeder::class,
TodoTypeSeeder::class,
UnitSeeder::class,
+ LicenseSeeder::class,
]);
$user = User::factory()->create([
diff --git a/database/seeders/LicenseSeeder.php b/database/seeders/LicenseSeeder.php
new file mode 100644
index 0000000..a7cf076
--- /dev/null
+++ b/database/seeders/LicenseSeeder.php
@@ -0,0 +1,17 @@
+count(10)->create();
+ }
+}
\ No newline at end of file
diff --git a/resources/js/components/AppSidebar.vue b/resources/js/components/AppSidebar.vue
index fd31ac7..b25e594 100644
--- a/resources/js/components/AppSidebar.vue
+++ b/resources/js/components/AppSidebar.vue
@@ -3,12 +3,12 @@ import NavFooter from '@/components/NavFooter.vue';
import NavMain from '@/components/NavMain.vue';
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarTrigger } from '@/components/ui/crm-sidebar';
import { useSidebar } from '@/components/ui/crm-sidebar/utils'
-import { dashboard, pipeline, offers, invoices, newInvoice, products, timesheets, customers, leads, achievements } from '@/routes';
+import { dashboard, pipeline, licenses, offers, invoices, newInvoice, products, timesheets, customers, leads, achievements } from '@/routes';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { type NavGroup } from '@/types';
import { Link, usePage } from '@inertiajs/vue3';
-import { Kanban, Euro, Trophy, Calculator, Timer, ContactRound, Headset, Plus, Package } from 'lucide-vue-next';
+import { Kanban, Euro, Trophy, Calculator, Timer, ContactRound, Headset, Plus, Package, FileBadge } from 'lucide-vue-next';
import AppLogo from './AppLogo.vue';
import { computed } from 'vue';
@@ -66,6 +66,11 @@ const mainNavGroups: NavGroup[] = [
href: products(),
icon: Package,
},
+ {
+ title: 'Lizenzen',
+ href: licenses(),
+ icon: FileBadge,
+ },
{
title: 'Zeiterfassung',
href: timesheets(),
diff --git a/resources/js/components/ui/crm-badge/index.ts b/resources/js/components/ui/crm-badge/index.ts
index fe746af..b1908f2 100644
--- a/resources/js/components/ui/crm-badge/index.ts
+++ b/resources/js/components/ui/crm-badge/index.ts
@@ -14,6 +14,8 @@ export const badgeVariants = cva(
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-destructive-foreground [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ success:
+ "border-transparent bg-success text-success-foreground [a&]:hover:bg-success/90 focus-visible:ring-success/20 dark:focus-visible:ring-success/40 dark:bg-success/60",
warning:
"border-transparent bg-warning text-warning-foreground [a&]:hover:bg-warning/90 focus-visible:ring-warning/20 dark:focus-visible:ring-warning/40 dark:bg-warning/60",
outline:
diff --git a/resources/js/pages/Licenses.vue b/resources/js/pages/Licenses.vue
new file mode 100644
index 0000000..a4ccbce
--- /dev/null
+++ b/resources/js/pages/Licenses.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Neue Lizenz anlegen
+
+ ⌘ N
+ Ctrl N
+
+
+
+
+
+
+
+
+
+
+
+ Gültig bis
+ Produkt
+ Version
+ Lizenznehmer
+
+
+
+
+
+
+
+
+
+
+ Permanent
+
+
+ {{ toLocalDate(license.expirationDate || '') }}
+
+
+ {{ toLocalDate(license.expirationDate || '') }}
+
+
+
+ {{ license.product }}
+
+
+ {{ license.version }}
+
+
+
+ {{ license.name }}
+
+
+ {{ license.email
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/services/LicenseService.ts b/resources/js/services/LicenseService.ts
new file mode 100644
index 0000000..503e7e5
--- /dev/null
+++ b/resources/js/services/LicenseService.ts
@@ -0,0 +1,126 @@
+import axios, { AxiosError } from 'axios';
+import { License } from '@/types';
+import { toast } from 'vue-sonner';
+
+const API_URL = '/api/licenses';
+
+export default {
+ /**
+ * Fetches all licenses
+ * @returns Promise
+ */
+ async getLicenses(): Promise {
+ try {
+ const response = await axios.get(API_URL);
+ return response.data;
+ } catch (error) {
+ toast.error('Fehler beim Abrufen der Lizenzen', { description: (error as AxiosError).message });
+ console.error(error);
+ return null;
+ }
+ },
+
+ /**
+ * Fetches a single license by ID
+ * @param id - The ID of the license to fetch
+ * @returns Promise
+ */
+ async getLicense(id: number): Promise {
+ try {
+ const response = await axios.get(`${API_URL}/${id}`);
+ return response.data;
+ } catch (error) {
+ toast.error('Fehler beim Abrufen der Lizenz', { description: (error as AxiosError).message });
+ console.error(error);
+ return null;
+ }
+ },
+
+ /**
+ * Creates a new license
+ * @param license - The license data to create
+ * @returns Promise
+ */
+ async createLicense(license: License): Promise {
+ try {
+ const response = await axios.post(API_URL, license);
+ return response.data;
+ } catch (error) {
+ toast.error('Fehler beim Erstellen der Lizenz', { description: (error as AxiosError).message });
+ console.error(error);
+ return null;
+ }
+ },
+
+ /**
+ * Updates an existing license
+ * @param license - The license data to update
+ * @returns Promise
+ */
+ async updateLicense(license: License): Promise {
+ try {
+ const response = await axios.put(`${API_URL}/${license.id}`, license);
+ return response.data;
+ } catch (error) {
+ toast.error('Fehler beim Aktualisieren der Lizenz', { description: (error as AxiosError).message });
+ console.error(error);
+ return null;
+ }
+ },
+
+ /**
+ * Deletes a license by ID
+ * @param id - The ID of the license to delete
+ * @returns Promise
+ */
+ async deleteLicense(id: number): Promise {
+ try {
+ await axios.delete(`${API_URL}/${id}`);
+ return true;
+ } catch (error) {
+ toast.error('Fehler beim Löschen der Lizenz', { description: (error as AxiosError).message });
+ console.error(error);
+ return false;
+ }
+ },
+
+ /**
+ * Downloads a license as a JSON file
+ * @param id - The ID of the license to download
+ * @returns Promise
+ */
+ async downloadLicense(id: number): Promise {
+ try {
+ const response = await axios.get(`/licenses/download/${id}`, {
+ responseType: 'blob',
+ });
+
+ const url = window.URL.createObjectURL(new Blob([response.data]));
+ const link = document.createElement('a');
+ link.href = url;
+ link.setAttribute('download', `license_${id}.json`);
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ } catch (error) {
+ toast.error('Fehler beim Herunterladen der Lizenz', { description: (error as AxiosError).message });
+ console.error(error);
+ }
+ },
+
+ /**
+ * Validates a license
+ * @param licenseId - The ID of the license to validate
+ * @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> {
+ try {
+ const response = await axios.post('/api/licenses/validate', { license_id: licenseId });
+ return response.data;
+ } catch (error) {
+ toast.error('Fehler beim Validieren der Lizenz', { description: (error as AxiosError).message });
+ console.error(error);
+ return null;
+ }
+ }
+};
\ No newline at end of file
diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts
index 72997f9..6037cf2 100644
--- a/resources/js/types/index.d.ts
+++ b/resources/js/types/index.d.ts
@@ -289,6 +289,34 @@ export function newBillingData() {
}
}
+export interface License {
+ id: number;
+ name: string;
+ email: string;
+ product: string;
+ version: string;
+ isPerpetual: boolean;
+ expirationDate: string | null;
+ signature: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export function newLicense(): License {
+ return {
+ id: 0,
+ name: '',
+ email: '',
+ product: '',
+ version: '',
+ isPerpetual: false,
+ expirationDate: null,
+ signature: '',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
+ };
+}
+
export interface LineItem {
id: number,
invoiceId: number,
@@ -411,7 +439,6 @@ export function newProduct(): Product {
}
}
-
// Pipeline item used by CRM pipeline
export interface PipelineItem {
id: number;
diff --git a/routes/api.php b/routes/api.php
index 1527cf8..0f40343 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -2,19 +2,20 @@
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\CustomerController;
-use App\Http\Controllers\NoteController;
use App\Http\Controllers\InvoiceController;
+use App\Http\Controllers\LicenseController;
use App\Http\Controllers\LineItemController;
+use App\Http\Controllers\NoteController;
use App\Http\Controllers\PaymentTermsController;
+use App\Http\Controllers\PipelineController;
+use App\Http\Controllers\PipelineItemController;
use App\Http\Controllers\ProductController;
use App\Http\Controllers\SettingController;
+use App\Http\Controllers\TimesheetController;
+use App\Http\Controllers\TimesheetEntryController;
use App\Http\Controllers\TodoController;
use App\Http\Controllers\UnitController;
use App\Mail\OrderConfirmation;
-use App\Http\Controllers\TimesheetController;
-use App\Http\Controllers\TimesheetEntryController;
-use App\Http\Controllers\PipelineController;
-use App\Http\Controllers\PipelineItemController;
Route::get('/pipeline', [PipelineController::class, 'index']);
Route::post('/pipeline/positions', [PipelineController::class, 'updatePositions']);
@@ -55,6 +56,12 @@
Route::delete('/invoices/{id}', [InvoiceController::class, 'delete']);
Route::get('/invoices/{id}/remind', [InvoiceController::class, 'remind']);
+Route::get('/licenses', [\App\Http\Controllers\LicenseController::class, 'index']);
+Route::get('/licenses/{id}', [\App\Http\Controllers\LicenseController::class, 'single']);
+Route::delete('/licenses/{id}', [\App\Http\Controllers\LicenseController::class, 'destroy']);
+Route::post('/licenses', [\App\Http\Controllers\LicenseController::class, 'store']);
+Route::put('/licenses/{id}', [\App\Http\Controllers\LicenseController::class, 'update']);
+
Route::get('/lineitems/{invoiceId}', [LineItemController::class, 'index']);
Route::post('/lineitems/import', [LineItemController::class, 'importFromCsv']);
diff --git a/routes/web.php b/routes/web.php
index 8272cca..6b88aa1 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -11,6 +11,7 @@
use App\Http\Controllers\ProductController;
use App\Http\Controllers\TimesheetController;
use App\Http\Controllers\PipelineController;
+use App\Http\Controllers\LicenseController;
Route::middleware('auth')->group(function () {
@@ -49,6 +50,10 @@
Route::get('invoice/{id}/pdf', [InvoiceController::class, 'exportPdf'])->name('invoiceExportPdf');
Route::get('invoice/{id}/xml', [InvoiceController::class, 'exportXml'])->name('invoiceExportXml');
+ // Licenses
+ Route::get('licenses', [LicenseController::class, 'show'])->name('licenses');
+ Route::get('licenses/download/{id}', [LicenseController::class, 'download'])->name('downloadLicense');
+
// Products
Route::get('products', [ProductController::class, 'show'])->name('products');