admin intregration
This commit is contained in:
parent
49e1a5d20f
commit
643aafe07d
@ -3,6 +3,8 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\App;
|
||||
use App\Models\AppRestriction;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
@ -18,14 +20,112 @@ public function index()
|
||||
// Fetch all users to display on the dashboard for administration
|
||||
$users = User::orderBy('created_at', 'desc')->get();
|
||||
|
||||
// Fetch all apps and active restrictions
|
||||
$apps = App::orderBy('name', 'asc')->get();
|
||||
$restrictions = AppRestriction::with('app')->orderBy('created_at', 'desc')->get();
|
||||
|
||||
// Calculate statistics
|
||||
$stats = [
|
||||
'total_users' => User::count(),
|
||||
'admins_count' => User::where('role', 'admin')->count(),
|
||||
'microsoft_users' => User::whereNotNull('microsoft_id')->count(),
|
||||
'credentials_users' => User::whereNull('microsoft_id')->count(),
|
||||
'blocked_count' => User::where('is_blocked', true)->count(),
|
||||
'apps_count' => App::count(),
|
||||
];
|
||||
|
||||
return view('admin.dashboard', compact('admin', 'users', 'stats'));
|
||||
return view('admin.dashboard', compact('admin', 'users', 'apps', 'restrictions', 'stats'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new app / service.
|
||||
*/
|
||||
public function storeApp(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'url' => 'required|url|max:255',
|
||||
'icon_svg' => 'nullable|string',
|
||||
'color' => 'nullable|string|max:20',
|
||||
'desc' => 'nullable|string',
|
||||
'tag' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
App::create([
|
||||
'name' => $request->name,
|
||||
'url' => $request->url,
|
||||
'icon_svg' => $request->icon_svg,
|
||||
'color' => $request->color ?? '#4f46e5',
|
||||
'desc' => $request->desc,
|
||||
'tag' => $request->tag ?? 'General',
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.dashboard')->with('success', 'Service app registered successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an app / service.
|
||||
*/
|
||||
public function destroyApp($id)
|
||||
{
|
||||
$app = App::findOrFail($id);
|
||||
$app->delete();
|
||||
|
||||
return redirect()->route('admin.dashboard')->with('success', 'Service app deleted successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrict an app for a specific user email.
|
||||
*/
|
||||
public function storeRestriction(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email|max:255',
|
||||
'app_id' => 'required|exists:apps,id',
|
||||
]);
|
||||
|
||||
// Prevent duplicate restrictions
|
||||
$exists = AppRestriction::where('email', $request->email)
|
||||
->where('app_id', $request->app_id)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return redirect()->route('admin.dashboard')->with('error', 'This user is already restricted from accessing this service.');
|
||||
}
|
||||
|
||||
AppRestriction::create([
|
||||
'email' => $request->email,
|
||||
'app_id' => $request->app_id,
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.dashboard')->with('success', 'Access restriction created successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an app restriction.
|
||||
*/
|
||||
public function destroyRestriction($id)
|
||||
{
|
||||
$restriction = AppRestriction::findOrFail($id);
|
||||
$restriction->delete();
|
||||
|
||||
return redirect()->route('admin.dashboard')->with('success', 'Access restriction removed successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the blocked status of a user.
|
||||
*/
|
||||
public function toggleBlockUser(Request $request, $id)
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
// Security: Prevent blocking oneself
|
||||
if ($user->id === Auth::id()) {
|
||||
return redirect()->route('admin.dashboard')->with('error', 'Security violation: You cannot block your own administrator account.');
|
||||
}
|
||||
|
||||
$user->is_blocked = !$user->is_blocked;
|
||||
$user->save();
|
||||
|
||||
$status = $user->is_blocked ? 'blocked' : 'unblocked';
|
||||
return redirect()->route('admin.dashboard')->with('success', "User account has been {$status} successfully.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,33 +14,13 @@ public function index()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Service list with URLs and aesthetic tags
|
||||
$services = [
|
||||
[
|
||||
'name' => 'Outlook',
|
||||
'url' => 'https://outlook.office.com/mail/',
|
||||
'icon_svg' => 'outlook',
|
||||
'color' => '#0078d4',
|
||||
'desc' => 'Access your corporate mail, calendar, and contacts instantly.',
|
||||
'tag' => 'Microsoft 365'
|
||||
],
|
||||
[
|
||||
'name' => 'Microsoft Teams',
|
||||
'url' => 'https://teams.microsoft.com/v2/',
|
||||
'icon_svg' => 'teams',
|
||||
'color' => '#6264a7',
|
||||
'desc' => 'Chat, meet, call, and collaborate with your team.',
|
||||
'tag' => 'Microsoft 365'
|
||||
],
|
||||
[
|
||||
'name' => 'Keka HR',
|
||||
'url' => 'https://sentientgeeks.keka.com/',
|
||||
'icon_svg' => 'keka',
|
||||
'color' => '#f27059',
|
||||
'desc' => 'Manage timesheets, leaves, payroll, and performance tasks.',
|
||||
'tag' => 'HR Portal'
|
||||
],
|
||||
];
|
||||
// Get all apps that are NOT restricted for this user's email
|
||||
$services = \App\Models\App::whereNotExists(function ($query) use ($user) {
|
||||
$query->select(\DB::raw(1))
|
||||
->from('app_restrictions')
|
||||
->whereColumn('app_restrictions.app_id', 'apps.id')
|
||||
->where('app_restrictions.email', $user->email);
|
||||
})->orderBy('name', 'asc')->get();
|
||||
|
||||
return view('dashboard', compact('user', 'services'));
|
||||
}
|
||||
|
||||
@ -22,6 +22,18 @@ public function showLoginForm()
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the administrator login screen.
|
||||
*/
|
||||
public function showAdminLoginForm()
|
||||
{
|
||||
if (Auth::check()) {
|
||||
return $this->redirectUserBasedOnRole(Auth::user());
|
||||
}
|
||||
|
||||
return view('auth.admin-login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Email & Password login for Administrators.
|
||||
*/
|
||||
@ -88,14 +100,20 @@ public function handleMicrosoftCallback(Request $request)
|
||||
return redirect()->route('login')->with('error', 'Microsoft Authentication failed or was cancelled: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$email = $microsoftUser->getEmail();
|
||||
$isSentientGeeks = str_ends_with($email, '@sentientgeeks.com') || str_ends_with($email, '@sentientgeeks.in');
|
||||
|
||||
// Search for user by microsoft_id first, then by email
|
||||
$user = User::where('microsoft_id', $microsoftUser->getId())->first();
|
||||
|
||||
if (!$user) {
|
||||
$user = User::where('email', $microsoftUser->getEmail())->first();
|
||||
$user = User::where('email', $email)->first();
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
if ($user->is_blocked) {
|
||||
return redirect()->route('login')->with('error', 'Your account has been blocked. Please contact the administrator.');
|
||||
}
|
||||
// Update existing user with Microsoft data if not already set
|
||||
$user->update([
|
||||
'microsoft_id' => $microsoftUser->getId(),
|
||||
@ -104,10 +122,15 @@ public function handleMicrosoftCallback(Request $request)
|
||||
'avatar' => $microsoftUser->getAvatar() ?? $user->avatar,
|
||||
]);
|
||||
} else {
|
||||
// Block registration if domain is not allowed
|
||||
if (!$isSentientGeeks) {
|
||||
return redirect()->route('login')->with('error', 'Access denied. Only @sentientgeeks.com and @sentientgeeks.in users are allowed to register.');
|
||||
}
|
||||
|
||||
// Create a new User (defaults to 'user' role)
|
||||
$user = User::create([
|
||||
'name' => $microsoftUser->getName() ?? explode('@', $microsoftUser->getEmail())[0],
|
||||
'email' => $microsoftUser->getEmail(),
|
||||
'name' => $microsoftUser->getName() ?? explode('@', $email)[0],
|
||||
'email' => $email,
|
||||
'microsoft_id' => $microsoftUser->getId(),
|
||||
'microsoft_token' => $microsoftUser->token,
|
||||
'microsoft_refresh_token' => $microsoftUser->refreshToken ?? null,
|
||||
|
||||
@ -19,6 +19,13 @@ public function handle(Request $request, Closure $next, string $role): Response
|
||||
return redirect()->route('login')->with('error', 'Please log in to access this page.');
|
||||
}
|
||||
|
||||
if (auth()->user()->is_blocked) {
|
||||
auth()->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
return redirect()->route('login')->with('error', 'Your account has been blocked. Please contact the administrator.');
|
||||
}
|
||||
|
||||
if (auth()->user()->role !== $role) {
|
||||
if (auth()->user()->role === 'admin') {
|
||||
return redirect()->route('admin.dashboard');
|
||||
|
||||
21
app/Models/App.php
Normal file
21
app/Models/App.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
|
||||
#[Fillable(['name', 'url', 'icon_svg', 'color', 'desc', 'tag'])]
|
||||
class App extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Get the restrictions for this app.
|
||||
*/
|
||||
public function restrictions()
|
||||
{
|
||||
return $this->hasMany(AppRestriction::class, 'app_id');
|
||||
}
|
||||
}
|
||||
21
app/Models/AppRestriction.php
Normal file
21
app/Models/AppRestriction.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
|
||||
#[Fillable(['email', 'app_id'])]
|
||||
class AppRestriction extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Get the app that is restricted.
|
||||
*/
|
||||
public function app()
|
||||
{
|
||||
return $this->belongsTo(App::class, 'app_id');
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,7 @@
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
#[Fillable(['name', 'email', 'password', 'role', 'microsoft_id', 'microsoft_token', 'microsoft_refresh_token', 'avatar'])]
|
||||
#[Fillable(['name', 'email', 'password', 'role', 'microsoft_id', 'microsoft_token', 'microsoft_refresh_token', 'avatar', 'is_blocked'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
<?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::table('users', function (Blueprint $table) {
|
||||
$table->boolean('is_blocked')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('is_blocked');
|
||||
});
|
||||
}
|
||||
};
|
||||
33
database/migrations/2026_06_23_091722_create_apps_table.php
Normal file
33
database/migrations/2026_06_23_091722_create_apps_table.php
Normal file
@ -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
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('apps', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('url');
|
||||
$table->longText('icon_svg')->nullable();
|
||||
$table->string('color')->default('#4f46e5');
|
||||
$table->text('desc')->nullable();
|
||||
$table->string('tag')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('apps');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?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('app_restrictions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('email');
|
||||
$table->foreignId('app_id')->constrained('apps')->onDelete('cascade');
|
||||
$table->unique(['email', 'app_id']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('app_restrictions');
|
||||
}
|
||||
};
|
||||
@ -36,5 +36,39 @@ public function run(): void
|
||||
'microsoft_id' => 'mock-microsoft-id-123',
|
||||
]
|
||||
);
|
||||
|
||||
// Seed default apps
|
||||
\App\Models\App::updateOrCreate(
|
||||
['name' => 'Outlook'],
|
||||
[
|
||||
'url' => 'https://outlook.office.com/mail/',
|
||||
'icon_svg' => 'outlook',
|
||||
'color' => '#0078d4',
|
||||
'desc' => 'Access your corporate mail, calendar, and contacts instantly.',
|
||||
'tag' => 'Microsoft 365'
|
||||
]
|
||||
);
|
||||
|
||||
\App\Models\App::updateOrCreate(
|
||||
['name' => 'Microsoft Teams'],
|
||||
[
|
||||
'url' => 'https://teams.microsoft.com/v2/',
|
||||
'icon_svg' => 'teams',
|
||||
'color' => '#6264a7',
|
||||
'desc' => 'Chat, meet, call, and collaborate with your team.',
|
||||
'tag' => 'Microsoft 365'
|
||||
]
|
||||
);
|
||||
|
||||
\App\Models\App::updateOrCreate(
|
||||
['name' => 'Keka HR'],
|
||||
[
|
||||
'url' => 'https://sentientgeeks.keka.com/',
|
||||
'icon_svg' => 'keka',
|
||||
'color' => '#f27059',
|
||||
'desc' => 'Manage timesheets, leaves, payroll, and performance tasks.',
|
||||
'tag' => 'HR Portal'
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -423,27 +423,192 @@
|
||||
|
||||
<div class="stat-card">
|
||||
<div>
|
||||
<div class="stat-label">Microsoft Connected</div>
|
||||
<div class="stat-value">{{ $stats['microsoft_users'] }}</div>
|
||||
<div class="stat-label">Blocked Accounts</div>
|
||||
<div class="stat-value">{{ $stats['blocked_count'] }}</div>
|
||||
</div>
|
||||
<div class="stat-icon" style="color: var(--success-color);">☁️</div>
|
||||
<div class="stat-icon" style="color: var(--error-color);">🚫</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div>
|
||||
<div class="stat-label">Local Auth Only</div>
|
||||
<div class="stat-value">{{ $stats['credentials_users'] }}</div>
|
||||
<div class="stat-label">Registered Apps</div>
|
||||
<div class="stat-value">{{ $stats['apps_count'] }}</div>
|
||||
</div>
|
||||
<div class="stat-icon" style="color: #fbbf24;">🔑</div>
|
||||
<div class="stat-icon" style="color: #fbbf24;">📦</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Alert messages in dashboard -->
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger" style="margin-bottom: 24px;">
|
||||
<span class="alert-icon">⚠️</span>
|
||||
<div>{{ session('error') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success" style="margin-bottom: 24px;">
|
||||
<span class="alert-icon">✓</span>
|
||||
<div>{{ session('success') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- App Management & Access Restrictions Grid -->
|
||||
<section class="management-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 20px; margin-bottom: 40px;">
|
||||
|
||||
<!-- App Management Card -->
|
||||
<div class="admin-card" style="background-color: var(--card-bg); border: 1px solid var(--card-border); border-radius: 20px; padding: 30px; backdrop-filter: blur(16px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);">
|
||||
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 600; margin-bottom: 16px; color: #a5b4fc;">Manage System Services</h3>
|
||||
|
||||
<!-- Add App Form -->
|
||||
<form action="{{ route('admin.apps.store') }}" method="POST" style="margin-bottom: 30px;">
|
||||
@csrf
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px;">
|
||||
<div>
|
||||
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Service Name</label>
|
||||
<input type="text" name="name" required placeholder="e.g. Keka HR" class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">SSO URL</label>
|
||||
<input type="url" name="url" required placeholder="https://..." class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px;">
|
||||
<div>
|
||||
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Theme Color</label>
|
||||
<input type="text" name="color" placeholder="#f27059" class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Portal Tag</label>
|
||||
<input type="text" name="tag" placeholder="HR Portal" class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Icon SVG Code or Preset Name (outlook/teams/keka)</label>
|
||||
<textarea name="icon_svg" rows="2" placeholder="e.g. keka, outlook, teams, or raw <svg>...</svg>" class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white; font-family:monospace; font-size:0.8rem;"></textarea>
|
||||
</div>
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Service Description</label>
|
||||
<input type="text" name="desc" placeholder="Brief description of the service..." class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||
</div>
|
||||
<button type="submit" class="btn" style="background-color: var(--accent-primary); color: white; width:100%; font-size:0.85rem; border-radius:8px; padding:10px;">+ Register Service App</button>
|
||||
</form>
|
||||
|
||||
<!-- Registered Apps List -->
|
||||
<h4 style="font-size:0.9rem; font-weight:600; margin-bottom:12px; border-top:1px solid var(--card-border); padding-top:20px; color:#9ca3af;">Registered Service Apps</h4>
|
||||
<div class="table-responsive" style="max-height: 250px; overflow-y: auto;">
|
||||
<table class="users-table" style="font-size:0.8rem;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service</th>
|
||||
<th>Tag</th>
|
||||
<th>URL</th>
|
||||
<th style="text-align: right;">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($apps as $app)
|
||||
<tr>
|
||||
<td>
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<div style="width:12px; height:12px; border-radius:50%; background-color:{{ $app->color }};"></div>
|
||||
<strong>{{ $app->name }}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="badge" style="background-color:rgba(255,255,255,0.05); color:#cbd5e1; border:1px solid rgba(255,255,255,0.1);">{{ $app->tag }}</span></td>
|
||||
<td><span style="color:var(--text-muted); display:inline-block; max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ $app->url }}</span></td>
|
||||
<td style="text-align: right;">
|
||||
<form action="{{ route('admin.apps.destroy', $app->id) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this service? All access restrictions for it will be removed.')" style="display:inline;">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="action-btn" style="color:#ef4444; border-color:rgba(239,68,68,0.2); font-size:0.75rem; padding:4px 8px;">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center; color: var(--text-muted); padding: 20px;">No registered service apps.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Access Restrictions Card -->
|
||||
<div class="admin-card" style="background-color: var(--card-bg); border: 1px solid var(--card-border); border-radius: 20px; padding: 30px; backdrop-filter: blur(16px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);">
|
||||
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 600; margin-bottom: 16px; color: #a5b4fc;">Restrict User Access</h3>
|
||||
|
||||
<!-- Add Restriction Form -->
|
||||
<form action="{{ route('admin.restrictions.store') }}" method="POST" style="margin-bottom: 30px;">
|
||||
@csrf
|
||||
<div style="margin-bottom: 12px;">
|
||||
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">User Email Address</label>
|
||||
<input type="email" name="email" required placeholder="user@sentientgeeks.com" class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||
</div>
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Restrict Service App</label>
|
||||
<select name="app_id" required class="form-input" style="width:100%; padding:10px; background:rgba(11,15,25,0.9); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||
<option value="">-- Select App to Restrict --</option>
|
||||
@foreach($apps as $app)
|
||||
<option value="{{ $app->id }}">{{ $app->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn" style="background-color: #fbbf24; color: #0b0f19; width:100%; font-size:0.85rem; border-radius:8px; padding:10px; font-weight:700;">Restrict Access</button>
|
||||
</form>
|
||||
|
||||
<!-- Active Restrictions List -->
|
||||
<h4 style="font-size:0.9rem; font-weight:600; margin-bottom:12px; border-top:1px solid var(--card-border); padding-top:20px; color:#9ca3af;">Active Access Restrictions</h4>
|
||||
<div class="table-responsive" style="max-height: 250px; overflow-y: auto;">
|
||||
<table class="users-table" style="font-size:0.8rem;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User Email</th>
|
||||
<th>Restricted App</th>
|
||||
<th style="text-align: right;">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($restrictions as $res)
|
||||
<tr>
|
||||
<td><strong style="color:#fca5a5;">{{ $res->email }}</strong></td>
|
||||
<td>
|
||||
@if($res->app)
|
||||
<span style="display:flex; align-items:center; gap:6px;">
|
||||
<div style="width:8px; height:8px; border-radius:50%; background-color:{{ $res->app->color }};"></div>
|
||||
{{ $res->app->name }}
|
||||
</span>
|
||||
@else
|
||||
<span style="color:#ef4444;">App Deleted</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align: right;">
|
||||
<form action="{{ route('admin.restrictions.destroy', $res->id) }}" method="POST" style="display:inline;">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="action-btn" style="color:#fbbf24; border-color:rgba(245,158,11,0.2); font-size:0.75rem; padding:4px 8px;">Remove</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3" style="text-align: center; color: var(--text-muted); padding: 20px;">No active restrictions. All apps visible to all users.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Users Table Card -->
|
||||
<section class="users-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">User Accounts Directory</div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted);">
|
||||
Showing registered system entities
|
||||
Showing registered system entities and block controls
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -471,7 +636,14 @@
|
||||
</div>
|
||||
@endif
|
||||
<div>
|
||||
<div class="user-name">{{ $usr->name }}</div>
|
||||
<div class="user-name" style="display:flex; align-items:center; gap:8px;">
|
||||
{{ $usr->name }}
|
||||
@if($usr->is_blocked)
|
||||
<span class="badge" style="background-color:rgba(239,68,68,0.15); color:#fca5a5; border:1px solid rgba(239,68,68,0.3); font-size:0.65rem; padding:2px 6px;">Blocked</span>
|
||||
@else
|
||||
<span class="badge" style="background-color:rgba(16,185,129,0.12); color:#a7f3d0; border:1px solid rgba(16,185,129,0.25); font-size:0.65rem; padding:2px 6px;">Active</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="user-email">{{ $usr->email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -504,7 +676,18 @@
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: right;">
|
||||
<button class="action-btn" onclick="alert('Management feature toggled for {{ $usr->name }}')">Configure</button>
|
||||
@if($usr->id !== $admin->id)
|
||||
<form action="{{ route('admin.users.toggle-block', $usr->id) }}" method="POST" style="display:inline;">
|
||||
@csrf
|
||||
<button type="submit" class="action-btn"
|
||||
style="font-size:0.75rem; padding:6px 12px; border-radius:6px; font-weight:600; cursor:pointer; transition:all 0.2s ease;
|
||||
{{ $usr->is_blocked ? 'color:#10b981; border-color:rgba(16,185,129,0.2); background-color:rgba(16,185,129,0.05);' : 'color:#ef4444; border-color:rgba(239,68,68,0.2); background-color:rgba(239,68,68,0.05);' }}">
|
||||
{{ $usr->is_blocked ? 'Unblock User' : 'Block User' }}
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<span style="font-size:0.75rem; color:var(--text-muted); font-style:italic;">You (Admin)</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
|
||||
390
resources/views/auth/admin-login.blade.php
Normal file
390
resources/views/auth/admin-login.blade.php
Normal file
@ -0,0 +1,390 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SingleLogin Portal - Admin Access</title>
|
||||
<!-- Google Fonts: Outfit & Inter -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Outfit:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #0b0f19;
|
||||
--card-bg: rgba(17, 24, 39, 0.7);
|
||||
--card-border: rgba(255, 255, 255, 0.08);
|
||||
--accent-primary: #ef4444; /* red for admin alert */
|
||||
--accent-secondary: #0078d4;
|
||||
--text-main: #f3f4f6;
|
||||
--text-muted: #9ca3af;
|
||||
--error-color: #ef4444;
|
||||
--success-color: #10b981;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Gorgeous glowing red gradient backgrounds for Admin */
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(239, 68, 68, 0.1) 0%, rgba(0, 0, 0, 0) 70%);
|
||||
top: -100px;
|
||||
left: -100px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(0, 120, 212, 0.1) 0%, rgba(0, 0, 0, 0) 70%);
|
||||
bottom: -150px;
|
||||
right: -150px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
padding: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.brand-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #fca5a5 0%, #ef4444 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
letter-spacing: -0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.brand-logo svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
fill: #ef4444;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* Glassmorphism Card styling */
|
||||
.login-card {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 20px;
|
||||
padding: 40px 32px;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.login-card:hover {
|
||||
box-shadow: 0 15px 40px rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: block;
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
/* Typography & Forms */
|
||||
h2 {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: 8px;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.portal-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 10px;
|
||||
color: var(--text-main);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
background-color: rgba(255, 255, 255, 0.07);
|
||||
box-shadow: 0 0 10px rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 13px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--accent-primary);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #dc2626;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 15px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-microsoft {
|
||||
background-color: #2f2f2f;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-main);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-microsoft:hover {
|
||||
background-color: #3f3f3f;
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.microsoft-icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.micro-rect {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
.r1 { background-color: #f25022; }
|
||||
.r2 { background-color: #7fba00; }
|
||||
.r3 { background-color: #00a4ef; }
|
||||
.r4 { background-color: #ffb900; }
|
||||
|
||||
/* Notification Alerts */
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.divider::before, .divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
}
|
||||
|
||||
.divider:not(:empty)::before {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.divider:not(:empty)::after {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="login-container">
|
||||
|
||||
<div class="brand-header">
|
||||
<div class="brand-logo">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8H17V6A5,5 0 0,0 12,1A5,5 0 0,0 7,6V8H6A2,2 0 0,0 4,10V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V10A2,2 0 0,0 18,8M8.9,6C8.9,4.29 10.29,2.9 12,2.9C13.71,2.9 15.1,4.29 15.1,6V8H8.9V6M18,20H6V10H18V20Z" />
|
||||
</svg>
|
||||
SingleLogin Admin
|
||||
</div>
|
||||
<div class="brand-subtitle">Administrator Access Only</div>
|
||||
</div>
|
||||
|
||||
<div class="login-card">
|
||||
|
||||
<!-- Alert Display -->
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger">
|
||||
<span class="alert-icon">⚠️</span>
|
||||
<div>{{ session('error') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success">
|
||||
<span class="alert-icon">✓</span>
|
||||
<div>{{ session('success') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<span class="alert-icon">⚠️</span>
|
||||
<div>
|
||||
@foreach($errors->all() as $error)
|
||||
<p>{{ $error }}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Admin Portal Content -->
|
||||
<div class="tab-content">
|
||||
<h2>Admin Credentials</h2>
|
||||
<div class="portal-desc">Manage system services, restrict users, and review platform authentication logs.</div>
|
||||
|
||||
<!-- Email/Password Admin Form -->
|
||||
<form action="{{ route('admin.login.submit') }}" method="POST">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">Admin Email</label>
|
||||
<input type="email" id="email" name="email" class="form-input" placeholder="admin@company.com" required value="{{ old('email') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Password</label>
|
||||
<input type="password" id="password" name="password" class="form-input" placeholder="••••••••" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="margin-bottom: 10px;">
|
||||
Login as Administrator
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="divider">or</div>
|
||||
|
||||
<!-- Microsoft Login Alternative for Admin -->
|
||||
<a href="{{ route('auth.microsoft.redirect') }}" class="btn btn-microsoft" style="text-decoration: none;">
|
||||
<div class="microsoft-icon-grid">
|
||||
<div class="micro-rect r1"></div>
|
||||
<div class="micro-rect r2"></div>
|
||||
<div class="micro-rect r3"></div>
|
||||
<div class="micro-rect r4"></div>
|
||||
</div>
|
||||
Sign in with Microsoft
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
© 2026 SingleLogin Systems. Secured with Microsoft Identity Platform.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -374,13 +374,7 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Portal Tabs -->
|
||||
<div class="portal-tabs">
|
||||
<button type="button" class="tab-btn active" onclick="switchPortal('user')">User Portal</button>
|
||||
<button type="button" class="tab-btn" onclick="switchPortal('admin')">Admin Portal</button>
|
||||
</div>
|
||||
|
||||
<!-- User Portal Tab -->
|
||||
<!-- User Portal Content -->
|
||||
<div id="user-portal" class="tab-content active">
|
||||
<h2>Welcome back</h2>
|
||||
<div class="portal-desc">Access your Microsoft 365 services and corporate resources. Users must sign in via Single Sign-On.</div>
|
||||
@ -396,43 +390,6 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Admin Portal Tab -->
|
||||
<div id="admin-portal" class="tab-content">
|
||||
<h2>Admin Login</h2>
|
||||
<div class="portal-desc">Manage tenant configurations, user assignments, and systems.</div>
|
||||
|
||||
<!-- Email/Password Admin Form -->
|
||||
<form action="{{ route('login.admin') }}" method="POST">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">Admin Email</label>
|
||||
<input type="email" id="email" name="email" class="form-input" placeholder="admin@company.com" required value="{{ old('email') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Password</label>
|
||||
<input type="password" id="password" name="password" class="form-input" placeholder="••••••••" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="margin-bottom: 10px;">
|
||||
Login as Administrator
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="divider">or</div>
|
||||
|
||||
<!-- Microsoft Login Alternative for Admin -->
|
||||
<a href="{{ route('auth.microsoft.redirect') }}" class="btn btn-microsoft" style="text-decoration: none;">
|
||||
<div class="microsoft-icon-grid">
|
||||
<div class="micro-rect r1"></div>
|
||||
<div class="micro-rect r2"></div>
|
||||
<div class="micro-rect r3"></div>
|
||||
<div class="micro-rect r4"></div>
|
||||
</div>
|
||||
Sign in with Microsoft
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
@ -441,21 +398,5 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchPortal(portal) {
|
||||
// Remove active classes
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
|
||||
// Add active classes
|
||||
if (portal === 'user') {
|
||||
document.querySelector('.tab-btn:nth-child(1)').classList.add('active');
|
||||
document.getElementById('user-portal').classList.add('active');
|
||||
} else {
|
||||
document.querySelector('.tab-btn:nth-child(2)').classList.add('active');
|
||||
document.getElementById('admin-portal').classList.add('active');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -564,6 +564,12 @@
|
||||
<svg viewBox="0 0 24 24" fill="{{ $service['color'] }}">
|
||||
<path d="M12.5,9A2.5,2.5 0 0,1 15,11.5A2.5,2.5 0 0,1 12.5,14A2.5,2.5 0 0,1 10,11.5A2.5,2.5 0 0,1 12.5,9M12.5,15.5C14.71,15.5 18.5,16.5 18.5,18.5V20H6.5V18.5C6.5,16.5 10.29,15.5 12.5,15.5M6,8A2,2 0 0,1 8,10A2,2 0 0,1 6,12A2,2 0 0,1 4,10A2,2 0 0,1 6,8M6,13.5C7.43,13.5 10,14.07 10,15.25V17H2V15.25C2,14.07 4.57,13.5 6,13.5M12.5,3A2,2 0 0,1 14.5,5A2,2 0 0,1 12.5,7A2,2 0 0,1 10.5,5A2,2 0 0,1 12.5,3M12.5,20V22H19V20H12.5Z" />
|
||||
</svg>
|
||||
@elseif($service['icon_svg'] === 'keka')
|
||||
<svg viewBox="0 0 24 24" fill="{{ $service['color'] }}">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H7c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.04-.42 1.99-1.07 2.75z"/>
|
||||
</svg>
|
||||
@elseif(str_starts_with($service['icon_svg'] ?? '', '<svg'))
|
||||
{!! $service['icon_svg'] !!}
|
||||
@else
|
||||
<svg viewBox="0 0 24 24" fill="{{ $service['color'] }}">
|
||||
<path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4M12,6A6,6 0 0,0 6,12A6,6 0 0,0 12,18A6,6 0 0,0 18,12A6,6 0 0,0 12,6M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8Z" />
|
||||
|
||||
@ -8,7 +8,8 @@
|
||||
// Authentication & Portal landing
|
||||
Route::get('/', [LoginController::class, 'showLoginForm']);
|
||||
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
|
||||
Route::post('/login/admin', [LoginController::class, 'adminLogin'])->name('login.admin');
|
||||
Route::get('/admin/login', [LoginController::class, 'showAdminLoginForm'])->name('admin.login');
|
||||
Route::post('/admin/login', [LoginController::class, 'adminLogin'])->name('admin.login.submit');
|
||||
Route::post('/logout', [LoginController::class, 'logout'])->name('logout');
|
||||
Route::get('/logout', [LoginController::class, 'logout']); // For ease of development/testing via GET
|
||||
|
||||
@ -24,4 +25,9 @@
|
||||
// Admin Control Panel (requires admin role)
|
||||
Route::middleware(['auth', 'role:admin'])->group(function () {
|
||||
Route::get('/admin/dashboard', [AdminController::class, 'index'])->name('admin.dashboard');
|
||||
Route::post('/admin/apps', [AdminController::class, 'storeApp'])->name('admin.apps.store');
|
||||
Route::delete('/admin/apps/{id}', [AdminController::class, 'destroyApp'])->name('admin.apps.destroy');
|
||||
Route::post('/admin/restrictions', [AdminController::class, 'storeRestriction'])->name('admin.restrictions.store');
|
||||
Route::delete('/admin/restrictions/{id}', [AdminController::class, 'destroyRestriction'])->name('admin.restrictions.destroy');
|
||||
Route::post('/admin/users/{id}/toggle-block', [AdminController::class, 'toggleBlockUser'])->name('admin.users.toggle-block');
|
||||
});
|
||||
|
||||
105
tests/Feature/SecurityAndAdminTest.php
Normal file
105
tests/Feature/SecurityAndAdminTest.php
Normal file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\App;
|
||||
use App\Models\AppRestriction;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SecurityAndAdminTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Test admin role middleware restrictions.
|
||||
*/
|
||||
public function test_admin_can_access_admin_dashboard_but_not_standard_users(): void
|
||||
{
|
||||
$admin = User::create([
|
||||
'name' => 'Admin User',
|
||||
'email' => 'admin@company.com',
|
||||
'role' => 'admin',
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => 'Standard User',
|
||||
'email' => 'user@sentientgeeks.com',
|
||||
'role' => 'user',
|
||||
]);
|
||||
|
||||
// Unauthenticated user is redirected to login
|
||||
$this->get('/admin/dashboard')
|
||||
->assertRedirect(route('login'));
|
||||
|
||||
// Admin can access admin dashboard
|
||||
$this->actingAs($admin)
|
||||
->get('/admin/dashboard')
|
||||
->assertStatus(200);
|
||||
|
||||
// Standard user gets redirected to user dashboard
|
||||
$this->actingAs($user)
|
||||
->get('/admin/dashboard')
|
||||
->assertRedirect(route('dashboard'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test blocked users are forced to logout and redirected.
|
||||
*/
|
||||
public function test_blocked_user_cannot_access_dashboard_and_gets_logged_out(): void
|
||||
{
|
||||
$user = User::create([
|
||||
'name' => 'Standard User',
|
||||
'email' => 'user@sentientgeeks.com',
|
||||
'role' => 'user',
|
||||
'is_blocked' => true,
|
||||
]);
|
||||
|
||||
// Attempting to access dashboard logs them out and redirects
|
||||
$this->actingAs($user)
|
||||
->get('/dashboard')
|
||||
->assertRedirect(route('login'));
|
||||
|
||||
$this->assertFalse(auth()->check());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test restricted services are hidden from specific users.
|
||||
*/
|
||||
public function test_restricted_service_is_not_displayed_to_the_restricted_user(): void
|
||||
{
|
||||
$user = User::create([
|
||||
'name' => 'Standard User',
|
||||
'email' => 'user@sentientgeeks.com',
|
||||
'role' => 'user',
|
||||
]);
|
||||
|
||||
$app1 = App::create([
|
||||
'name' => 'Outlook',
|
||||
'url' => 'https://outlook.office.com',
|
||||
'color' => '#0078d4',
|
||||
'tag' => 'Microsoft 365'
|
||||
]);
|
||||
|
||||
$app2 = App::create([
|
||||
'name' => 'Keka HR',
|
||||
'url' => 'https://sentientgeeks.keka.com',
|
||||
'color' => '#f27059',
|
||||
'tag' => 'HR Portal'
|
||||
]);
|
||||
|
||||
// Restrict user from Keka HR
|
||||
AppRestriction::create([
|
||||
'email' => $user->email,
|
||||
'app_id' => $app2->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get('/dashboard');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('Outlook');
|
||||
$response->assertDontSee('Keka HR');
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user