diff --git a/README.md b/README.md new file mode 100644 index 0000000..af05a91 --- /dev/null +++ b/README.md @@ -0,0 +1,303 @@ +# Company SSO — Admin Panel + +A single sign-on (SSO) administration panel built with **Laravel 13**, **Livewire 4**, and **Flux UI 2**. It provides user management, authentication flows (including two-factor authentication), profile & security settings, and a dashboard — all rendered server-side with reactive Livewire components. + +--- + +## Table of Contents + +- [Tech Stack](#tech-stack) +- [Requirements](#requirements) +- [Installation](#installation) +- [Running the Application](#running-the-application) +- [Available Commands](#available-commands) +- [Project Structure](#project-structure) +- [Authentication Features](#authentication-features) +- [Testing](#testing) +- [Code Style](#code-style) +- [Libraries & Documentation](#libraries--documentation) +- [License](#license) + +--- + +## Tech Stack + +| Layer | Technology | Version | +| ---------- | --------------------------- | ------- | +| Framework | Laravel | 13.x | +| PHP | PHP | 8.3+ | +| Frontend | Livewire | 4.x | +| CSS | Tailwind CSS | 4.x | +| Bundler | Vite | 8.x | +| Auth | Laravel Fortify | 1.x | +| Icons | Blade Lucide Icons | 1.x | +| DTOs | Spatie Laravel Data | 4.x | +| Testing | Pest | 4.x | +| Linting | Laravel Pint | 1.x | +| Database | SQLite (default) / MySQL | — | + +--- + +## Requirements + +- **PHP** ≥ 8.3 +- **Composer** ≥ 2.x +- **Node.js** ≥ 20.x & **npm** ≥ 10.x +- **SQLite** (default) or any Laravel-supported database + +--- + +## Installation + +### 1. Clone the repository + +```bash +git clone sentientgeeks_admin_sso +cd sentientgeeks_admin_sso +``` + +### 2. Install PHP dependencies + +```bash +composer install +``` + +### 3. Configure environment + +```bash +cp .env.example .env +php artisan key:generate +``` + +Edit `.env` to configure your database, mail, and other services as needed. The default configuration uses **SQLite** — no extra setup required. + +### 4. Create the database + +For SQLite (default): + +```bash +touch database/database.sqlite +``` + +For MySQL/PostgreSQL, update the `DB_*` variables in `.env` accordingly. + +### 5. Run migrations + +```bash +php artisan migrate +``` + +### 6. Install Node dependencies & build assets + +```bash +npm install +npm run build +``` + +### Quick Setup (all-in-one) + +Alternatively, run the bundled setup script that performs steps 2–6 automatically: + +```bash +composer setup +``` + +--- + +## Running the Application + +### Development (recommended) + +Start **all** development services concurrently — web server, queue worker, log tail, and Vite dev server: + +```bash +composer run dev +``` + +This launches: + +| Service | URL / Info | +| ------------- | ---------------------------------- | +| Laravel server | `http://localhost:8000` | +| Queue worker | Listens with `--tries=1` | +| Pail (logs) | Real-time log streaming | +| Vite | HMR & asset compilation | + +### Production build + +```bash +npm run build +php artisan serve +``` + +--- + +## Available Commands + +| Command | Description | +| --------------------- | -------------------------------------------------------- | +| `composer setup` | Full project setup (install, env, migrate, build) | +| `composer run dev` | Start all dev services concurrently | +| `composer run lint` | Fix code style with Pint | +| `composer run lint:check` | Check code style without modifying files | +| `composer run test` | Clear config, lint-check, and run all tests | +| `npm run dev` | Start Vite dev server with HMR | +| `npm run build` | Build production assets | + +--- + +## Livewire Commands +```bash + # Create a new Livewire component, follow dot (.) notation for sub-folders in name + php artisan make:livewire + + # Create a page component (full-page Livewire component) + php artisan make:livewire pages:: --page +``` + +## Project Structure + +``` +├── app/ +│ ├── Actions/Fortify/ # Auth action classes (create user, reset password) +│ ├── Concerns/ # Shared traits (validation rules) +│ ├── Data/Ui/ # Spatie Data DTOs for UI (Connected Services) +│ ├── Enums/ # ConnectionStatus, ConnectionTechTypes +│ ├── Http/Controllers/ # HTTP controllers +│ ├── Livewire/ +│ │ ├── Actions/ # Livewire action classes (Logout) +│ │ └── Settings/ # Settings components (Profile, Security, Appearance, 2FA) +│ ├── Models/ # Eloquent models (User) +│ └── Providers/ # Service providers (App, Fortify) +├── config/ # Application configuration +├── database/ +│ ├── factories/ # Model factories +│ ├── migrations/ # Database migrations +│ └── seeders/ # Database seeders +├── resources/ +│ ├── css/ # Tailwind CSS entry point +│ ├── js/ # JavaScript entry point +│ └── views/ +│ ├── components/ # Blade components (sidebar, topbar, logos) +│ ├── flux/ # Flux UI component overrides +│ ├── layouts/ # Application layouts +│ ├── livewire/ +│ │ ├── auth/ # Auth views (login, register, 2FA, etc.) +│ │ └── settings/ # Settings views (profile, security, appearance) +│ ├── pages/ # Full-page Livewire components (dashboard) +│ └── partials/ # Reusable view partials +├── routes/ +│ ├── web.php # Main web routes +│ ├── settings.php # Settings routes (profile, security, appearance) +│ └── console.php # Artisan console routes +├── tests/ +│ ├── Feature/ # Feature tests (Auth, Settings, Dashboard) +│ └── Unit/ # Unit tests +├── vite.config.js # Vite + Tailwind CSS v4 + Laravel plugin config +├── composer.json +└── package.json +``` + +--- + +## Authentication Features + +Powered by **Laravel Fortify**, the application supports: + +- **Registration** — New user sign-up +- **Login / Logout** — Session-based authentication with rate limiting +- **Password Reset** — Forgot password & reset via email +- **Email Verification** — Verify email address flow +- **Two-Factor Authentication (2FA)** — TOTP-based with QR codes and recovery codes +- **Password Confirmation** — Re-confirm password before sensitive actions +- **Profile Management** — Update name, email +- **Account Deletion** — Self-service account removal + +--- + +## Testing + +This project uses **Pest** for testing. + +```bash +# Run all tests +php artisan test --compact + +# Run a specific test file +php artisan test --compact --filter=LoginTest + +# Full CI check (lint + test) +composer run test +``` + +--- + +## Code Style + +Code style is enforced with **Laravel Pint**. After modifying PHP files: + +```bash +# Auto-fix formatting +vendor/bin/pint --parallel + +# Check without modifying +vendor/bin/pint --test +``` + +--- + +## Libraries & Documentation + +### Core Framework + +| Library | Description | Docs | +| --- | --- | --- | +| **Laravel 13** | PHP web application framework | [laravel.com/docs](https://laravel.com/docs) | +| **PHP 8.3+** | Server-side language | [php.net/docs](https://www.php.net/docs.php) | + +### Frontend & UI + +| Library | Description | Docs | +| --- | --- | --- | +| **Livewire 4** | Full-stack reactive components for Laravel | [livewire.laravel.com/docs](https://livewire.laravel.com/docs) | +| **Tailwind CSS 4** | Utility-first CSS framework | [tailwindcss.com/docs](https://tailwindcss.com/docs) | +| **Alpine.js** | Lightweight JS framework (bundled with Livewire) | [alpinejs.dev](https://alpinejs.dev/) | +| **Blade Lucide Icons** | Lucide icon set for Blade templates | [github.com/mallardduck/blade-lucide-icons](https://github.com/mallardduck/blade-lucide-icons) | + +### Authentication + +| Library | Description | Docs | +| --- | --- | --- | +| **Laravel Fortify 1** | Backend authentication scaffolding | [laravel.com/docs/fortify](https://laravel.com/docs/fortify) | + +### Data & Architecture + +| Library | Description | Docs | +| --- | --- | --- | +| **Spatie Laravel Data 4** | Typed data transfer objects | [spatie.be/docs/laravel-data](https://spatie.be/docs/laravel-data) | +| **Tailwind Merge Laravel** | Intelligent Tailwind class merging in PHP | [github.com/gehrisandro/tailwind-merge-laravel](https://github.com/gehrisandro/tailwind-merge-laravel) | + +### Build Tools + +| Library | Description | Docs | +| --- | --- | --- | +| **Vite 8** | Next-generation frontend build tool | [vite.dev/guide](https://vite.dev/guide/) | +| **Laravel Vite Plugin 3** | Laravel integration for Vite | [laravel.com/docs/vite](https://laravel.com/docs/vite) | + +### Development & Testing + +| Library | Description | Docs | +| --- | --- | --- | +| **Pest 4** | Elegant PHP testing framework | [pestphp.com/docs](https://pestphp.com/docs) | +| **Laravel Pint 1** | Opinionated PHP code style fixer | [laravel.com/docs/pint](https://laravel.com/docs/pint) | +| **Laravel Sail 1** | Docker development environment | [laravel.com/docs/sail](https://laravel.com/docs/sail) | +| **Laravel Pail 1** | Real-time log viewer | [laravel.com/docs/pail](https://laravel.com/docs/pail) | +| **Laravel Tinker 3** | REPL for Laravel | [laravel.com/docs/artisan#tinker](https://laravel.com/docs/artisan#tinker) | +| **FakerPHP** | Fake data generator for testing | [fakerphp.org](https://fakerphp.org/) | +| **Mockery** | Mock object framework for PHP | [docs.mockery.io](https://docs.mockery.io/) | + +--- + +## License + +This project is proprietary software. All rights reserved. diff --git a/app/Data/Ui/ManageRoles/RoleData.php b/app/Data/Ui/ManageRoles/RoleData.php new file mode 100644 index 0000000..a3c5685 --- /dev/null +++ b/app/Data/Ui/ManageRoles/RoleData.php @@ -0,0 +1,22 @@ + + */ +final class RolesCollection extends Collection {} diff --git a/app/Data/Ui/ManageRoles/RolesData.php b/app/Data/Ui/ManageRoles/RolesData.php new file mode 100644 index 0000000..b8c36f3 --- /dev/null +++ b/app/Data/Ui/ManageRoles/RolesData.php @@ -0,0 +1,17 @@ + + */ +final class ServiceCollection extends Collection {} diff --git a/app/Data/Ui/ManageRoles/ServiceData.php b/app/Data/Ui/ManageRoles/ServiceData.php new file mode 100644 index 0000000..46a73fb --- /dev/null +++ b/app/Data/Ui/ManageRoles/ServiceData.php @@ -0,0 +1,18 @@ + + */ +final class UserCollection extends Collection {} diff --git a/app/Data/Users/UserData.php b/app/Data/Users/UserData.php new file mode 100644 index 0000000..2176714 --- /dev/null +++ b/app/Data/Users/UserData.php @@ -0,0 +1,16 @@ +registerScopeDirective(); + } + public function registerScopeDirective(): void + { + /** + * All credits from this blade directive goes to Konrad Kalemba. + * Just copied and modified for my very specific use case. + * + * https://github.com/konradkalemba/blade-components-scoped-slots + */ + Blade::directive('scope', function ($expression) { + // Split the expression by `top-level` commas (not in parentheses) + $directiveArguments = preg_split("/,(?![^\(\(]*[\)\)])/", $expression); + $directiveArguments = array_map('trim', $directiveArguments); + + [$name, $functionArguments] = $directiveArguments; + + // Build function "uses" to inject extra external variables + $uses = Arr::except(array_flip($directiveArguments), [$name, $functionArguments]); + $uses = array_flip($uses); + $uses[] = '$__env'; + $uses[] = '$__bladeCompiler'; + $uses = implode(',', $uses); + + /** + * Slot names can`t contains dot , eg: `user.city`. + * So we convert `user.city` to `user___city` + * + * Later, on component it will be replaced back. + */ + $name = str_replace('.', '___', $name); + + return "slot({$name}, function({$functionArguments}) use ({$uses}) { \$loop = !empty(\$__env->getLoopStack()) ? (object) \$__env->getLoopStack()[0] : null; ?>"; + }); + + Blade::directive('endscope', fn() => ''); + } +} diff --git a/app/View/Components/Table.php b/app/View/Components/Table.php new file mode 100644 index 0000000..c0db971 --- /dev/null +++ b/app/View/Components/Table.php @@ -0,0 +1,269 @@ +rowDecoration; + $cellDecoration = $this->cellDecoration; + $headers = $this->headers; + + // Remove closures from serialization + unset($this->rowDecoration, $this->cellDecoration, $this->headers); + + + + // Serialize a unique ID for component tracking + $this->uuid = "table-" . md5(serialize($this)) . $id; + + // Restore closures + $this->rowDecoration = $rowDecoration; + $this->cellDecoration = $cellDecoration; + $this->headers = $headers; + } + + // Check if header is hidden + public function isHidden(mixed $header): bool + { + return $header['hidden'] ?? false; + } + + // Format header contents + public function format(mixed $row, mixed $field, mixed $header): mixed + { + $format = $header['format'] ?? null; + + if ( ! $format) { + return $field; + } + + if (is_callable($format)) { + return $format($row, $field); + } + + if ('currency' === $format[0]) { + return ($format[2] ?? '') . number_format((float) $field, ...mb_str_split($format[1])); + } + + if ('date' === $format[0] && $field) { + return Carbon::parse($field)->translatedFormat($format[1]); + } + + return $field; + } + + // Check if link should be shown in cell + public function hasLink(mixed $header): bool + { + return $this->link && empty($header['disableLink']); + } + + // Build row link + public function redirectLink(mixed $row): string + { + $link = $this->link; + + // Transform from `route()` pattern + $link = Str::of($link)->replace('%5B', '{')->replace('%5D', '}'); + + // Extract tokens like {id}, {city.name} ... + $tokens = Str::of($link)->matchAll('/\{(.*?)\}/'); + + // Replace tokens with actual row values + $tokens->each(function (string $token) use ($row, &$link): void { + $link = Str::of($link)->replace("{" . $token . "}", data_get($row, $token))->toString(); + }); + + return $link; + } + + public function rowClasses(mixed $row): ?string + { + $classes = []; + + foreach ($this->rowDecoration as $class => $condition) { + if ($condition($row)) { + $classes[] = $class; + } + } + + return Arr::join($classes, ' '); + } + + public function cellClasses(mixed $row, array $header): ?string + { + $classes = Str::of($header['class'] ?? '')->explode(' ')->all(); + + foreach ($this->cellDecoration[$header['key']] ?? [] as $class => $condition) { + if ($condition($row)) { + $classes[] = $class; + } + } + + return Arr::join($classes, ' '); + } + + public function render(): View|Closure|string + { + return <<<'HTML' +
+
+ whereDoesntStartWith('wire:model') + ->class([ + 'w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400', + ]) + }} + > + $noHeaders + ])> + + @foreach($headers as $header) + @php + if($isHidden($header)) continue; + + # Scoped slot's name like `user.city` are compiled to `user___city` through `@scope / @endscope`. + # So we use current `$header` key to find that slot on context. + $temp_key = str_replace('.', '___', $header['key']) + @endphp + + + @endforeach + + @if($actions) + + @endif + + + + + @foreach($rows as $k => $row) + $striped, + 'hover:bg-gray-50' => !$noHover, + 'cursor-pointer' => $attributes->has('@row-click') || $link + ]) + @if($attributes->has('@row-click')) + @click="$dispatch('row-click', {{ json_encode($row) }});" + @endif + > + @foreach($headers as $header) + @php + if($isHidden($header)) continue; + $temp_key = str_replace('.', '___', $header['key']) + @endphp + + @if(isset(${"cell_".$temp_key})) + + @else + + @endif + @endforeach + + @if($actions) + + @endif + + @endforeach + + + @isset ($footer) + attributes->get('class') ?? '' + ])> + {{ $footer }} + + @endisset +
+ {{ isset(${"header_".$temp_key}) ? ${"header_".$temp_key}($header) : $header['label'] }} + + Actions +
$hasLink($header)])> + @if($hasLink($header)) + + @endif + + {{ ${"cell_".$temp_key}($row) }} + + @if($hasLink($header)) + + @endif + $hasLink($header)])> + @if($hasLink($header)) + + @endif + + {{ $format($row, data_get($row, $header['key']), $header) }} + + @if($hasLink($header)) + + @endif + + {{ $actions($row) }} +
+ + @if(count($rows) === 0) + @if($showEmptyText) +
+ {{ $emptyText }} +
+ @endif + @if($empty) +
+ {{ $empty }} +
+ @endif + @endif +
+ + @if($withPagination && method_exists($rows, 'links')) +
+ {{ $rows->links() }} +
+ @endif +
+ HTML; + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 4be20b2..7b0123d 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,10 +2,8 @@ declare(strict_types=1); -use App\Providers\AppServiceProvider; -use App\Providers\FortifyServiceProvider; - return [ - AppServiceProvider::class, - FortifyServiceProvider::class, + App\Providers\AppServiceProvider::class, + App\Providers\FortifyServiceProvider::class, + App\Providers\ScopeServiceProvider::class, ]; diff --git a/resources/views/components/connected-services/⚡all-connected-services.blade.php b/resources/views/components/dashboard/connected-services/⚡all.blade.php similarity index 100% rename from resources/views/components/connected-services/⚡all-connected-services.blade.php rename to resources/views/components/dashboard/connected-services/⚡all.blade.php diff --git a/resources/views/components/connected-services/⚡connected-services.blade.php b/resources/views/components/dashboard/connected-services/⚡connected-services.blade.php similarity index 66% rename from resources/views/components/connected-services/⚡connected-services.blade.php rename to resources/views/components/dashboard/connected-services/⚡connected-services.blade.php index c6caf28..f0bbe13 100644 --- a/resources/views/components/connected-services/⚡connected-services.blade.php +++ b/resources/views/components/dashboard/connected-services/⚡connected-services.blade.php @@ -2,29 +2,24 @@ use Livewire\Component; -new class extends Component -{ +new class extends Component { public string $selectedTab = 'all-tab'; }; ?>
- -
-

- - {{ __('Connected Services') }} -

+ +
{{ __('Add service') }}
-
+ - +
Tricks
diff --git a/resources/views/components/dashboard/roles/⚡manager.blade.php b/resources/views/components/dashboard/roles/⚡manager.blade.php new file mode 100644 index 0000000..d16602c --- /dev/null +++ b/resources/views/components/dashboard/roles/⚡manager.blade.php @@ -0,0 +1,134 @@ + + */ + public array $header; + public RolesData $rolesData; + + public function mount(): void + { + $this->header = [ + [ + 'key' => 'name', + 'label' => 'Role Name', + ], + [ + 'key' => 'users', + 'label' => 'User Assigned', + ], + [ + 'key' => 'services', + 'label' => 'Services', + ], + [ + 'key' => 'status', + 'label' => 'Status', + ] + ]; + + $this->rolesData = new RolesData( + roles: new RolesCollection([ + new RoleData( + name: 'Super Admin', + users: new UsersData( + users: new UserCollection([ + new UserData( + name: 'Kushal Saha', + email: 'kushal@example.com' + ), + new UserData( + name: 'Test Example', + email: 'test@example.com' + ) + ] + ) + ), + services: new ServiceCollection( + [ + new ServiceData( + name: 'Teams' + ) + ] + ), + status: \App\Enums\RolesStatus::Active + ), + new RoleData( + name: 'Super Admin', + users: new UsersData( + users: new UserCollection([ + new UserData( + name: 'Kushal Saha', + email: 'kushal@example.com' + ) + ] + ) + ), + services: new ServiceCollection( + [ + new ServiceData( + name: 'Teams' + ), + new ServiceData( + name: 'Outlook' + ) + ] + ), + status: \App\Enums\RolesStatus::Active + ), + ]) + ); + } +}; +?> + + +
+ + + +
+ + {{ __('Add role') }} +
+
+
+ + @scope('cell_users', $row) + @foreach($row->users->users as $user) + + {{implode('', array_map(fn($w) => strtoupper($w[0]), explode(' ', $user->name)))}} + + @endforeach + @endscope + @scope('cell_status', $row) + + {{ ucfirst($row->status->name) }} + + @endscope + @scope('cell_services', $row) + @foreach($row->services as $service) + + {{$service->name}} + + @endforeach + @endscope + +
+
diff --git a/resources/views/components/shared/badge.blade.php b/resources/views/components/shared/badge.blade.php new file mode 100644 index 0000000..6b322e0 --- /dev/null +++ b/resources/views/components/shared/badge.blade.php @@ -0,0 +1,3 @@ +twMerge(['class'=> 'px-4 py-1 rounded-full bg-gray-200'])}} +>{{ $slot }} diff --git a/resources/views/components/shared/card.blade.php b/resources/views/components/shared/card.blade.php index c8c00b1..1e625bc 100644 --- a/resources/views/components/shared/card.blade.php +++ b/resources/views/components/shared/card.blade.php @@ -1,3 +1,19 @@ +@props(['title', 'icon', 'actions'])
twMerge('rounded-xl border border-gray-200 p-4 shadow-xs bg-white')}}> + @isset($title) +
+

+ @isset($icon) + + @svg($icon) + + @endisset + {{ __($title) }} +

+ @isset($actions) + {{$actions}} + @endisset +
+ @endisset {{$slot}}
diff --git a/resources/views/components/shared/tabs/tabs.blade.php b/resources/views/components/shared/tabs/tabs.blade.php index 3b63bf3..980e73d 100644 --- a/resources/views/components/shared/tabs/tabs.blade.php +++ b/resources/views/components/shared/tabs/tabs.blade.php @@ -2,7 +2,7 @@ 'selected' => null, 'labelClass' => 'px-4 py-3 cursor-pointer', 'activeClass' => 'font-medium border-b border-b-gray-400', - 'labelDivClass' => 'border-b border-b-base-content/10 flex overflow-x-auto', + 'labelDivClass' => 'border-b bg-gray-50 border-b-base-content/10 flex overflow-x-auto', 'tabsClass' => 'relative w-full', ]) diff --git a/resources/views/layouts/app/sidebar.blade.php b/resources/views/layouts/app/sidebar.blade.php index 37b6cff..750990f 100644 --- a/resources/views/layouts/app/sidebar.blade.php +++ b/resources/views/layouts/app/sidebar.blade.php @@ -4,11 +4,13 @@ @include('partials.head') -
+
+
-
+ +
{{ $slot }}
diff --git a/resources/views/pages/⚡dashboard.blade.php b/resources/views/pages/⚡dashboard.blade.php index ae40408..5b9ef98 100644 --- a/resources/views/pages/⚡dashboard.blade.php +++ b/resources/views/pages/⚡dashboard.blade.php @@ -15,6 +15,7 @@ class extends Component {
- + +