diff --git a/laravel/app/Http/Controllers/WorkstationController.php b/laravel/app/Http/Controllers/WorkstationController.php new file mode 100644 index 0000000000000000000000000000000000000000..d54a55af73281bcfdacf6d0c3266a7803a0423a1 --- /dev/null +++ b/laravel/app/Http/Controllers/WorkstationController.php @@ -0,0 +1,28 @@ +<?php + +namespace App\Http\Controllers; + +use App\Models\Workstation; +use App\Http\Requests\StoreWorkstationRequest; +use Illuminate\Http\Request; + +class WorkstationController extends Controller +{ + public function index() + { + $workstations = Workstation::all(); + return view('pages.workstations', compact('workstations')); + } + + public function store(StoreWorkstationRequest $request) + { + Workstation::create($request->validated()); + return redirect()->back()->with('message', 'Workstation added successfully'); + } + + public function destroy(Workstation $workstation) + { + $workstation->delete(); + return redirect()->back()->with('message', 'Workstation deleted'); + } +} diff --git a/laravel/app/Http/Requests/StoreWorkstationRequest.php b/laravel/app/Http/Requests/StoreWorkstationRequest.php new file mode 100644 index 0000000000000000000000000000000000000000..119a6b3493277e6c520126074b9329e1968d2d8b --- /dev/null +++ b/laravel/app/Http/Requests/StoreWorkstationRequest.php @@ -0,0 +1,40 @@ +<?php + +namespace App\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Str; + +class StoreWorkstationRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + public function rules(): array + { + return [ + 'name' => 'required|string|max:255', + 'network_bridge' => 'required|string|max:255', + 'subnet' => 'required|string', // validated and split below + 'mask' => 'required|string|ip', // will be set based on subnet + ]; + } + + public function prepareForValidation(): void + { + if ($this->has('subnet')) { + // Normalize: 192.168.0.0/24 → subnet + mask + [$subnet, $cidr] = array_pad(explode('/', $this->input('subnet')), 2, null); + + if (filter_var($subnet, FILTER_VALIDATE_IP) && is_numeric($cidr)) { + $mask = long2ip(-1 << (32 - $cidr)); + $this->merge([ + 'subnet' => $subnet, + 'mask' => $mask, + ]); + } + } + } +} diff --git a/laravel/app/Models/Workstation.php b/laravel/app/Models/Workstation.php new file mode 100644 index 0000000000000000000000000000000000000000..466fd50148fc6094f53a2340a03fc52e86428942 --- /dev/null +++ b/laravel/app/Models/Workstation.php @@ -0,0 +1,10 @@ +<?php + +namespace App\Models; + +use Illuminate\Database\Eloquent\Model; + +class Workstation extends Model +{ + protected $fillable = ['name', 'network_bridge', 'subnet', 'mask']; +} diff --git a/laravel/database/migrations/2025_06_03_170217_create_workstations_table.php b/laravel/database/migrations/2025_06_03_170217_create_workstations_table.php new file mode 100644 index 0000000000000000000000000000000000000000..dcf977c76ae532e035489eef8c31589e97227693 --- /dev/null +++ b/laravel/database/migrations/2025_06_03_170217_create_workstations_table.php @@ -0,0 +1,31 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('workstations', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('network_bridge'); + $table->string('subnet'); + $table->string('mask'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('workstations'); + } +}; diff --git a/laravel/resources/css/pages/deploy.css b/laravel/resources/css/pages/deploy.css new file mode 100644 index 0000000000000000000000000000000000000000..0c14717a11d6321598586357d8868ce2284c49e8 --- /dev/null +++ b/laravel/resources/css/pages/deploy.css @@ -0,0 +1,10 @@ +@import 'tailwindcss'; + +.workstation { + @apply border border-black w-16 h-16 flex items-center justify-center rounded cursor-pointer; +} + +.selected { + background-color: #86efac; + /* Tailwind green-300 */ +} \ No newline at end of file diff --git a/laravel/resources/views/components/sidebar.blade.php b/laravel/resources/views/components/sidebar.blade.php index 3ca55c8f45d65bd5041e477730c354dacf5f4ad6..8f96f2ea66d76fd855a87406fa05ed4e47053342 100644 --- a/laravel/resources/views/components/sidebar.blade.php +++ b/laravel/resources/views/components/sidebar.blade.php @@ -2,15 +2,15 @@ // New structure: Grouped links $links = [ 'Home' => 'home', -'VMs' => [ -'Exercise Pool' => 'vm-pool', -'Workstations' => 'workstation-pool', -], 'User' => 'addusers', 'Images' => [ -'ID Pools' => 'id-pools.index', -'Repositories' => 'repos.index', -'Template VM Verwaltung' => 'templates.index', + 'ID Pools' => 'id-pools.index', + 'Repositories' => 'repos.index', + 'Template VM Verwaltung' => 'templates.index', +], +'VMs' => [ +'Workstations' => 'workstations.index', +'Deploy Images' => 'workstations.deploy' ], 'Flags' => 'flag', ]; diff --git a/laravel/resources/views/pages/deploy.blade.php b/laravel/resources/views/pages/deploy.blade.php new file mode 100644 index 0000000000000000000000000000000000000000..bc8eee7703a7d72bfb51d0c6727d03f0a6d81f7c --- /dev/null +++ b/laravel/resources/views/pages/deploy.blade.php @@ -0,0 +1,51 @@ +<x-layouts.app title="Deploy Images"> + @push('styles') + @vite('resources/css/pages/deploy.css') + @endpush + <h2 class="text-2xl font-bold mb-4">Deploy Images</h2> + <hr class="border-t-4 border-red-600 mb-6"> + + <form method="POST" action="" id="deployForm"> + @csrf + + <div class="flex items-center gap-4 mb-6"> + <select name="template_id" class="border rounded px-4 py-2"> + @foreach ($templates as $template) + <option value="{{ $template->id }}">{{ $template->name }}</option> + @endforeach + </select> + <span>deploy to Workstation:</span> + </div> + + <div class="grid grid-cols-5 gap-4 mb-6"> + @foreach ($workstations as $workstation) + <div class="workstation" + data-id="{{ $workstation->id }}" + onclick="toggleSelection(this)"> + {{ $workstation->name }} + </div> + @endforeach + </div> + + <!-- Hidden input to store selected workstation IDs --> + <input type="hidden" name="workstation_ids" id="workstation_ids"> + + <button type="submit" class="bg-black text-white px-6 py-2 rounded">Deploy</button> + </form> + + <script> + const selectedWorkstations = new Set(); + + function toggleSelection(element) { + const id = element.getAttribute('data-id'); + if (selectedWorkstations.has(id)) { + selectedWorkstations.delete(id); + element.classList.remove('selected'); + } else { + selectedWorkstations.add(id); + element.classList.add('selected'); + } + document.getElementById('workstation_ids').value = Array.from(selectedWorkstations).join(','); + } + </script> +</x-layouts.app> diff --git a/laravel/resources/views/pages/repos/index.blade.php b/laravel/resources/views/pages/repos/index.blade.php index 93bbdb988d6314c5ed3572e0adff5a46fd1f5b37..3d61cae338ecea1729842b8d0031ea1231daf693 100644 --- a/laravel/resources/views/pages/repos/index.blade.php +++ b/laravel/resources/views/pages/repos/index.blade.php @@ -74,4 +74,8 @@ class="w-full bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700 tra </a> </div> + <div class="mt-4"> + <p>Need help with access tokens? <a href="{{ route('repos.token-help') }}" class="text-blue-600 hover:text-blue-800 underline">Click here</a></p> + </div> + </x-layouts.app> diff --git a/laravel/resources/views/pages/workstations.blade.php b/laravel/resources/views/pages/workstations.blade.php new file mode 100644 index 0000000000000000000000000000000000000000..66d4ce4704f24b313354edda61c9c673d9950e63 --- /dev/null +++ b/laravel/resources/views/pages/workstations.blade.php @@ -0,0 +1,53 @@ +<x-layouts.app title="Workstations"> + <h2 class="title">Workstations</h2> + <p>Manage your workstations here.</p> + + @if(session('message')) + <div class="my-2 text-green-600">{{ session('message') }}</div> + @endif + + @if($errors->any()) + <div class="my-2 text-red-600">{{ $errors->first() }}</div> + @endif + + <form method="POST" action="{{ route('workstations.store') }}" class="my-4"> + @csrf + <div class="flex gap-2"> + <input name="name" placeholder="Name" class="border p-2 rounded" required> + <input name="network_bridge" placeholder="Network Bridge" class="border p-2 rounded" required> + <input name="subnet" placeholder="Subnet (e.g. 192.168.1.0/24)" class="border p-2 rounded" required> + <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">Add</button> + </div> + </form> + + <table class="table-auto w-full mt-4 border"> + <thead> + <tr class="bg-gray-100 text-left"> + <th class="p-2 border">Name</th> + <th class="p-2 border">Bridge</th> + <th class="p-2 border">Subnet</th> + <th class="p-2 border">Mask</th> + <th class="p-2 border">Actions</th> + </tr> + </thead> + <tbody> + @foreach($workstations as $ws) + <tr> + <td class="p-2 border">{{ $ws->name }}</td> + <td class="p-2 border">{{ $ws->network_bridge }}</td> + <td class="p-2 border">{{ $ws->subnet }}</td> + <td class="p-2 border">{{ $ws->mask }}</td> + <td class="p-2 border"> + <form method="POST" action="{{ route('workstations.destroy', $ws) }}"> + @csrf + @method('DELETE') + <button onclick="return confirm('Are you sure?')" class="bg-red-500 text-white px-2 py-1 rounded"> + Delete + </button> + </form> + </td> + </tr> + @endforeach + </tbody> + </table> +</x-layouts.app> diff --git a/laravel/routes/web.php b/laravel/routes/web.php index 85fc2c2f1796f65cb67ff28e8f2624361089675d..6d955155caee548f18331b4f49cdf5427bae02e1 100644 --- a/laravel/routes/web.php +++ b/laravel/routes/web.php @@ -6,6 +6,7 @@ use App\Http\Controllers\RepoController; use App\Http\Controllers\IdPoolController; use App\Http\Controllers\TemplateVMController; +use App\Http\Controllers\WorkstationController; Route::view('/', 'pages.home')->name('home'); @@ -33,4 +34,13 @@ // Template Management Route::get('/templates', [TemplateVMController::class, 'index'])->name('templates.index'); Route::post('/templates', [TemplateVMController::class, 'store'])->name('templates.store'); -Route::delete('/templates/{template}', [TemplateVMController::class, 'destroy'])->name('templates.destroy'); \ No newline at end of file +Route::delete('/templates/{template}', [TemplateVMController::class, 'destroy'])->name('templates.destroy'); + +// Workstation Management +Route::get('/workstations/deploy', function() { + return view('pages.deploy', [ + 'workstations' => \App\Models\Workstation::all(), + 'templates' => \App\Models\TemplateVM::all() + ]); +})->name('workstations.deploy'); +Route::resource('workstations', WorkstationController::class)->only(['index', 'store', 'destroy']);