Add Authentik SSO via OAuth2/OIDC
Implements Single Sign-On with Authentik alongside the existing password login. Admins configure Client ID, Client Secret and Base URL directly in the app; the SSO button on the login page only appears when the configuration is complete and enabled. - SsoSetting model + migration (one-row config table) - sso_provider_id / is_sso_user fields on users (migration) - SsoController: redirect to Authentik + callback (token exchange, userinfo fetch, auto-create unknown users) - Admin\SsoSettingController + admin/sso/show view with setup guide and one-click Redirect URI copy - Admin dropdown: SSO-Konfiguration entry - Login page: Authentik button rendered conditionally No additional Composer packages required. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
c5fe3ad808
commit
60259a3655
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SsoSetting;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SsoSettingController extends Controller
|
||||
{
|
||||
public function show()
|
||||
{
|
||||
$sso = SsoSetting::get();
|
||||
return view('admin.sso.show', compact('sso'));
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'base_url' => 'nullable|url|max:255',
|
||||
'client_id' => 'nullable|string|max:255',
|
||||
'client_secret' => 'nullable|string|max:255',
|
||||
'slug' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$sso = SsoSetting::get();
|
||||
|
||||
$sso->base_url = rtrim($request->base_url ?? '', '/') ?: null;
|
||||
$sso->client_id = $request->client_id ?: null;
|
||||
$sso->client_secret = $request->client_secret ?: null;
|
||||
$sso->slug = $request->slug ?: null;
|
||||
$sso->enabled = $request->boolean('enabled');
|
||||
$sso->save();
|
||||
|
||||
return redirect()->route('admin.sso.show')->with('success', 'SSO-Einstellungen gespeichert.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SsoSetting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SsoController extends Controller
|
||||
{
|
||||
public function redirect(Request $request)
|
||||
{
|
||||
$sso = SsoSetting::get();
|
||||
|
||||
if (!$sso->isConfigured()) {
|
||||
return redirect()->route('login')->with('error', 'SSO ist nicht konfiguriert.');
|
||||
}
|
||||
|
||||
$state = Str::random(40);
|
||||
session(['sso_state' => $state]);
|
||||
|
||||
$query = http_build_query([
|
||||
'client_id' => $sso->client_id,
|
||||
'redirect_uri' => route('sso.callback'),
|
||||
'response_type' => 'code',
|
||||
'scope' => 'openid email profile',
|
||||
'state' => $state,
|
||||
]);
|
||||
|
||||
return redirect($sso->authorizeUrl() . '?' . $query);
|
||||
}
|
||||
|
||||
public function callback(Request $request)
|
||||
{
|
||||
$sso = SsoSetting::get();
|
||||
|
||||
if (!$sso->isConfigured()) {
|
||||
return redirect()->route('login')->with('error', 'SSO ist nicht konfiguriert.');
|
||||
}
|
||||
|
||||
// State prüfen
|
||||
if ($request->get('state') !== session('sso_state')) {
|
||||
return redirect()->route('login')->with('error', 'Ungültige SSO-Anfrage (state mismatch).');
|
||||
}
|
||||
session()->forget('sso_state');
|
||||
|
||||
if ($request->has('error')) {
|
||||
return redirect()->route('login')->with('error', 'SSO-Anmeldung abgebrochen: ' . $request->get('error_description', $request->get('error')));
|
||||
}
|
||||
|
||||
// Code gegen Token tauschen
|
||||
$tokenResponse = Http::asForm()->post($sso->tokenUrl(), [
|
||||
'grant_type' => 'authorization_code',
|
||||
'client_id' => $sso->client_id,
|
||||
'client_secret' => $sso->client_secret,
|
||||
'redirect_uri' => route('sso.callback'),
|
||||
'code' => $request->get('code'),
|
||||
]);
|
||||
|
||||
if ($tokenResponse->failed()) {
|
||||
return redirect()->route('login')->with('error', 'SSO-Token konnte nicht abgerufen werden.');
|
||||
}
|
||||
|
||||
$accessToken = $tokenResponse->json('access_token');
|
||||
|
||||
// Userinfo abrufen
|
||||
$userInfo = Http::withToken($accessToken)->get($sso->userInfoUrl());
|
||||
|
||||
if ($userInfo->failed()) {
|
||||
return redirect()->route('login')->with('error', 'SSO-Benutzerinformationen konnten nicht abgerufen werden.');
|
||||
}
|
||||
|
||||
$info = $userInfo->json();
|
||||
$providerId = $info['sub'] ?? null;
|
||||
$email = $info['email'] ?? null;
|
||||
$name = $info['name'] ?? $info['preferred_username'] ?? $email;
|
||||
|
||||
if (!$providerId || !$email) {
|
||||
return redirect()->route('login')->with('error', 'Unvollständige Benutzerinformationen vom SSO-Provider.');
|
||||
}
|
||||
|
||||
// User suchen oder anlegen
|
||||
$user = User::where('sso_provider_id', $providerId)->first()
|
||||
?? User::where('email', $email)->first();
|
||||
|
||||
if (!$user) {
|
||||
$user = User::create([
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'password' => bcrypt(Str::random(32)),
|
||||
'sso_provider_id' => $providerId,
|
||||
'is_sso_user' => true,
|
||||
]);
|
||||
} elseif (!$user->sso_provider_id) {
|
||||
// Bestehenden Account mit SSO verknüpfen
|
||||
$user->sso_provider_id = $providerId;
|
||||
$user->is_sso_user = true;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
Auth::login($user, true);
|
||||
|
||||
return redirect()->intended(route('user-nail-polishes.index'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SsoSetting extends Model
|
||||
{
|
||||
protected $table = 'sso_settings';
|
||||
|
||||
protected $fillable = [
|
||||
'base_url',
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'slug',
|
||||
'enabled',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'enabled' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public static function get(): self
|
||||
{
|
||||
return static::firstOrCreate([]);
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return $this->enabled
|
||||
&& !empty($this->base_url)
|
||||
&& !empty($this->client_id)
|
||||
&& !empty($this->client_secret);
|
||||
}
|
||||
|
||||
public function authorizeUrl(): string
|
||||
{
|
||||
return rtrim($this->base_url, '/') . '/application/o/authorize/';
|
||||
}
|
||||
|
||||
public function tokenUrl(): string
|
||||
{
|
||||
return rtrim($this->base_url, '/') . '/application/o/token/';
|
||||
}
|
||||
|
||||
public function userInfoUrl(): string
|
||||
{
|
||||
return rtrim($this->base_url, '/') . '/application/o/userinfo/';
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -22,6 +22,8 @@ class User extends Authenticatable
|
||||
'email',
|
||||
'password',
|
||||
'is_admin',
|
||||
'sso_provider_id',
|
||||
'is_sso_user',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -43,7 +45,8 @@ class User extends Authenticatable
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'password' => 'hashed',
|
||||
'is_sso_user' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sso_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('base_url')->nullable();
|
||||
$table->string('client_id')->nullable();
|
||||
$table->string('client_secret')->nullable();
|
||||
$table->string('slug')->nullable()->comment('Authentik Application Slug');
|
||||
$table->boolean('enabled')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Leere Zeile anlegen, damit immer genau eine Konfiguration existiert
|
||||
DB::table('sso_settings')->insert([
|
||||
'enabled' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sso_settings');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('sso_provider_id')->nullable()->unique()->after('email');
|
||||
$table->boolean('is_sso_user')->default(false)->after('sso_provider_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn(['sso_provider_id', 'is_sso_user']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,222 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'SSO-Konfiguration – NeoNail DB')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-hero d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4">
|
||||
<div>
|
||||
<h1><i class="fas fa-shield-alt me-2"></i>Single Sign-On</h1>
|
||||
<p>Authentik OAuth2/OIDC Konfiguration</p>
|
||||
</div>
|
||||
<span class="count-chip">
|
||||
@if($sso->isConfigured())
|
||||
<i class="fas fa-circle text-success" style="font-size:.6rem"></i> Aktiv
|
||||
@else
|
||||
<i class="fas fa-circle text-danger" style="font-size:.6rem"></i> Nicht konfiguriert
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- Status-Banner --}}
|
||||
@if($sso->isConfigured())
|
||||
<div class="alert alert-success mb-4">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
SSO ist aktiv. Auf der Login-Seite wird der <strong>„Mit Authentik anmelden"</strong>-Button angezeigt.
|
||||
</div>
|
||||
@elseif($sso->enabled && !$sso->isConfigured())
|
||||
<div class="alert alert-warning mb-4">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
SSO ist aktiviert, aber die Konfiguration ist unvollständig. Bitte alle Felder ausfüllen.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{{-- Konfigurationsformular --}}
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-cog me-2"></i>Einstellungen
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" action="{{ route('admin.sso.update') }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
{{-- Aktivieren --}}
|
||||
<div class="mb-4">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
name="enabled" id="enabled" value="1"
|
||||
{{ $sso->enabled ? 'checked' : '' }}>
|
||||
<label class="form-check-label fw-semibold" for="enabled">
|
||||
SSO aktivieren
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text mt-1">
|
||||
Nur wenn aktiviert <em>und</em> alle Felder ausgefüllt sind, erscheint der SSO-Button auf der Login-Seite.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
{{-- Base URL --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="base_url">
|
||||
Authentik Base URL <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="url" class="form-control @error('base_url') is-invalid @enderror"
|
||||
id="base_url" name="base_url"
|
||||
value="{{ old('base_url', $sso->base_url) }}"
|
||||
placeholder="https://authentik.example.com">
|
||||
@error('base_url')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
<div class="form-text">Die URL deiner Authentik-Instanz, ohne abschließenden Slash.</div>
|
||||
</div>
|
||||
|
||||
{{-- Client ID --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="client_id">
|
||||
Client ID <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" class="form-control @error('client_id') is-invalid @enderror"
|
||||
id="client_id" name="client_id"
|
||||
value="{{ old('client_id', $sso->client_id) }}"
|
||||
placeholder="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
autocomplete="off">
|
||||
@error('client_id')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Client Secret --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="client_secret">
|
||||
Client Secret <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control @error('client_secret') is-invalid @enderror"
|
||||
id="client_secret" name="client_secret"
|
||||
value="{{ old('client_secret', $sso->client_secret) }}"
|
||||
placeholder="{{ $sso->client_secret ? '••••••••••••••••' : 'Client Secret eingeben' }}"
|
||||
autocomplete="off">
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
onclick="toggleSecret()" id="toggleSecretBtn">
|
||||
<i class="fas fa-eye" id="toggleSecretIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
@error('client_secret')
|
||||
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Slug --}}
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="slug">
|
||||
Application Slug
|
||||
<span class="badge bg-secondary ms-1" style="font-size:.65rem">optional</span>
|
||||
</label>
|
||||
<input type="text" class="form-control @error('slug') is-invalid @enderror"
|
||||
id="slug" name="slug"
|
||||
value="{{ old('slug', $sso->slug) }}"
|
||||
placeholder="neonail-db">
|
||||
@error('slug')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
<div class="form-text">Nur zur Dokumentation – wird nicht im OAuth2-Flow verwendet.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-2"></i>Speichern
|
||||
</button>
|
||||
@if($sso->isConfigured())
|
||||
<a href="{{ route('sso.redirect') }}" target="_blank" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-external-link-alt me-2"></i>SSO testen
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Info-Sidebar --}}
|
||||
<div class="col-lg-4">
|
||||
|
||||
{{-- Callback URL --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-link me-2"></i>Redirect URI
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<p style="font-size:.82rem; color:#6b7280; margin-bottom:8px;">
|
||||
Diese URL musst du in Authentik als <strong>Redirect URI</strong> eintragen:
|
||||
</p>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<code style="font-size:.78rem; background:#f3f4f6; padding:8px 10px; border-radius:8px; flex:1; word-break:break-all;">
|
||||
{{ route('sso.callback') }}
|
||||
</code>
|
||||
<button class="btn btn-sm btn-outline-secondary flex-shrink-0"
|
||||
onclick="copyRedirectUri()" title="Kopieren">
|
||||
<i class="fas fa-copy" id="copyIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Setup-Anleitung --}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-book me-2"></i>Setup in Authentik
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<ol style="font-size:.83rem; color:#374151; padding-left:1.2rem; margin:0; line-height:1.8;">
|
||||
<li>In Authentik: <strong>Applications → Create</strong></li>
|
||||
<li>Provider-Typ: <strong>OAuth2/OpenID Provider</strong></li>
|
||||
<li>Authorization flow wählen</li>
|
||||
<li>Redirect URI eintragen (siehe links)</li>
|
||||
<li><strong>Client ID</strong> und <strong>Client Secret</strong> kopieren</li>
|
||||
<li>Hier eintragen und speichern</li>
|
||||
</ol>
|
||||
|
||||
<hr style="margin: 12px 0;">
|
||||
|
||||
<p style="font-size:.78rem; color:#9ca3af; margin:0;">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Scopes: <code>openid email profile</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
function toggleSecret() {
|
||||
const input = document.getElementById('client_secret');
|
||||
const icon = document.getElementById('toggleSecretIcon');
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
icon.classList.replace('fa-eye', 'fa-eye-slash');
|
||||
} else {
|
||||
input.type = 'password';
|
||||
icon.classList.replace('fa-eye-slash', 'fa-eye');
|
||||
}
|
||||
}
|
||||
|
||||
function copyRedirectUri() {
|
||||
const uri = @json(route('sso.callback'));
|
||||
const icon = document.getElementById('copyIcon');
|
||||
navigator.clipboard.writeText(uri).then(() => {
|
||||
icon.classList.replace('fa-copy', 'fa-check');
|
||||
setTimeout(() => icon.classList.replace('fa-check', 'fa-copy'), 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
@@ -74,6 +74,21 @@
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@php $sso = \App\Models\SsoSetting::get(); @endphp
|
||||
@if($sso->isConfigured())
|
||||
<div class="d-flex align-items-center my-4" style="gap:12px;">
|
||||
<hr style="flex:1; border-color:rgba(0,0,0,.12); margin:0;">
|
||||
<span style="font-size:.78rem; color:#9ca3af; white-space:nowrap;">oder</span>
|
||||
<hr style="flex:1; border-color:rgba(0,0,0,.12); margin:0;">
|
||||
</div>
|
||||
<a href="{{ route('sso.redirect') }}" class="btn btn-outline-secondary w-100 btn-lg d-flex align-items-center justify-content-center gap-2">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20A14.5 14.5 0 0 0 12 2"/><path d="M2 12h20"/>
|
||||
</svg>
|
||||
Mit Authentik anmelden
|
||||
</a>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -471,6 +471,9 @@
|
||||
<i class="fas fa-palette me-2"></i>Nagellacke</a></li>
|
||||
<li><a class="dropdown-item" href="{{ route('admin.statistics') }}">
|
||||
<i class="fas fa-chart-bar me-2"></i>Statistiken</a></li>
|
||||
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.1);"></li>
|
||||
<li><a class="dropdown-item" href="{{ route('admin.sso.show') }}">
|
||||
<i class="fas fa-shield-alt me-2"></i>SSO-Konfiguration</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@@ -5,6 +5,8 @@ use App\Http\Controllers\NailPolishController;
|
||||
use App\Http\Controllers\UserNailPolishController;
|
||||
use App\Http\Controllers\AdminController;
|
||||
use App\Http\Controllers\Auth\LoginController;
|
||||
use App\Http\Controllers\Auth\SsoController;
|
||||
use App\Http\Controllers\Admin\SsoSettingController;
|
||||
use App\Http\Controllers\ManufacturerController;
|
||||
|
||||
// Startseite
|
||||
@@ -16,8 +18,12 @@ Route::get('/', function () {
|
||||
Route::middleware('guest')->group(function () {
|
||||
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
|
||||
Route::post('/login', [LoginController::class, 'login']);
|
||||
Route::get('/sso/redirect', [SsoController::class, 'redirect'])->name('sso.redirect');
|
||||
});
|
||||
|
||||
// SSO Callback (kein guest-Middleware, da Authentik zurückleitet)
|
||||
Route::get('/sso/callback', [SsoController::class, 'callback'])->name('sso.callback');
|
||||
|
||||
// Logout (nur für eingeloggte Benutzer)
|
||||
Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth');
|
||||
|
||||
@@ -42,6 +48,8 @@ Route::middleware(['auth'])->group(function () {
|
||||
Route::delete('/users/{user}', [AdminController::class, 'destroyUser'])->name('users.destroy');
|
||||
Route::get('/statistics', [AdminController::class, 'statistics'])->name('statistics');
|
||||
Route::get('/users/{user}/collection', [UserNailPolishController::class, 'showUserCollection'])->name('users.collection');
|
||||
Route::get('/sso', [SsoSettingController::class, 'show'])->name('sso.show');
|
||||
Route::put('/sso', [SsoSettingController::class, 'update'])->name('sso.update');
|
||||
});
|
||||
|
||||
// Nagellack-Verwaltung (nur für Admin)
|
||||
|
||||
Reference in New Issue
Block a user