| Current Path : /var/www/html/reddsis/docs/ |
| Current File : /var/www/html/reddsis/docs/CMS-AUTH-SYSTEM.md |
# CMS Auth System
Dokumen ini menerangkan sistem autentikasi (login, register, forgot/reset password, email verification) yang dibina menggunakan CMS Builder — di mana semua halaman auth boleh diedit terus dari Admin Panel tanpa perlu VS Code.
---
## Senibina
```
┌─────────────────────────────────────────────────────────┐
│ CMS Pages (Web Editor) │
│ /{site_slug}/login-web │
│ /{site_slug}/register-web │
│ /{site_slug}/forgot-password-web │
│ /{site_slug}/reset-password-web │
│ /{site_slug}/email-verify │
│ /{site_slug}/email-verify-confirm │
│ /{site_slug}/login (Controller approach) │
│ /{site_slug}/register (Controller approach) │
│ /{site_slug}/user-dashboard (Post-login page) │
├─────────────────────────────────────────────────────────┤
│ Route Redirects (Dynamic) │
│ /user/login → SettingHelper::authSiteSlug() │
│ /user/register → SettingHelper::authSiteSlug() │
│ /user/forgot-password → SettingHelper::authSiteSlug() │
│ /user/reset-password → SettingHelper::authSiteSlug() │
│ /user/email/verify → SettingHelper::authSiteSlug() │
├─────────────────────────────────────────────────────────┤
│ Controllers (POST Logic) │
│ LoginController@loginSubmit │
│ LoginController@logout │
│ LoginController@sendResetLink │
│ LoginController@resetPassword │
│ RegisterController@registerSubmit │
│ VerifyEmailController@resend │
├─────────────────────────────────────────────────────────┤
│ Models / Overrides │
│ FrontendUser@sendPasswordResetNotification │
│ FrontendUser@sendEmailVerificationNotification │
├─────────────────────────────────────────────────────────┤
│ Dynamic Site Slug Resolution │
│ SettingHelper::authSiteSlug() — dari request/session │
│ SettingHelper::defaultSiteSlug() — dari DB default site │
└─────────────────────────────────────────────────────────┘
```
---
## Dynamic Site Slug
Setiap site dalam CMS boleh ada halaman auth sendiri. Slug site dikesan secara dinamik melalui `SettingHelper::authSiteSlug()`:
```php
// app/Http/Helpers/SettingHelper.php
public static function authSiteSlug(): string
{
// 1. Cuba ambil dari hidden input form (site_slug)
$slug = request()->input('site_slug', request()->segment(1));
// 2. Jika slug = 'user' (dari route prefix), ignore
if ($slug && $slug !== 'user') {
return $slug;
}
// 3. Fallback ke default site dari DB
return self::defaultSiteSlug();
}
public static function defaultSiteSlug(): string
{
return \App\Models\Backend\FrontendSite::where('is_default', true)
->value('slug') ?? 'test';
}
```
**Aliran slug resolution:**
1. Hidden input `site_slug` dari form submission (Controller approach)
2. URL segment pertama (Web approach — self-POST)
3. Default site dari database
4. Fallback: `'test'`
---
## CMS Pages
Semua halaman auth adalah CMS Page dengan `use_custom_layout = true`. Layout mengandungi HTML penuh + `@php` block untuk logik.
### Site: Test (ID: 3, slug: `test`) — Web Approach
| Page | Slug | Approach | URL |
|---|---|---|---|
| Login Web | `login-web` | Direct @php | `/{site_slug}/login-web` |
| Register Web | `register-web` | Direct @php | `/{site_slug}/register-web` |
| Forgot Password Web | `forgot-password-web` | Direct @php | `/{site_slug}/forgot-password-web` |
| Reset Password Web | `reset-password-web` | Direct @php | `/{site_slug}/reset-password-web` |
| Email Verify | `email-verify` | Static notice | `/{site_slug}/email-verify` |
| Email Verify Confirm | `email-verify-confirm` | Direct @php | `/{site_slug}/email-verify-confirm` |
| User Dashboard | `user-dashboard` | Entry Script + Layout | `/{site_slug}/user-dashboard` |
### Site: Try (ID: 4, slug: `try`) — Controller Approach
| Page | Slug | Approach | URL |
|---|---|---|---|
| Login | `login` | Controller POST | `/{site_slug}/login` |
| Register | `register` | Controller POST | `/{site_slug}/register` |
| Forgot Password Web | `forgot-password-web` | Direct @php | `/{site_slug}/forgot-password-web` |
| Reset Password Web | `reset-password-web` | Direct @php | `/{site_slug}/reset-password-web` |
| Email Verify | `email-verify` | Static notice | `/{site_slug}/email-verify` |
| Email Verify Confirm | `email-verify-confirm` | Direct @php | `/{site_slug}/email-verify-confirm` |
---
## Dua Approach
### 1. Web Approach (Direct @php)
Guna `@php` block dalam page layout untuk handle semua logik (validation, auth, redirect). Form `action=""` (self-POST). Sesuai untuk standalone pages.
**Ciri-ciri:**
- Form POST ke page sendiri — URL kekal `/{site_slug}/{page_slug}`
- `request()->segment(1)` sentiasa return site slug yang betul
- Redirect guna `throw new HttpResponseException(redirect()->to($redirect))`
- Login redirect: `$redirect = "/" . request()->segment(1) . "/user-dashboard"`
### 2. Controller Approach (Laravel Controllers)
Guna Laravel controllers untuk handle POST logic. Page layout hanya display form. Form POST ke route `/user/{action}`.
**Ciri-ciri:**
- Form POST ke route Laravel: `route('user.login.submit')`, `route('user.register.submit')`
- Hidden field `site_slug` diperlukan untuk bawa context site:
```blade
<input type="hidden" name="site_slug" value="{{ request()->segment(1) }}">
```
- Controller redirect guna `SettingHelper::authSiteSlug()`
- Session disimpan di `RegisterController` untuk redirect lepas register
---
## Route Redirects
Semua route `/user/*` untuk display page (GET) di redirect ke CMS page menggunakan `SettingHelper::authSiteSlug()`. POST routes kekal guna controller.
| Route (GET) | Redirect Ke |
|---|---|
| `/user/login` | `/{authSiteSlug}/login-web` |
| `/user/register` | `/{authSiteSlug}/register-web` |
| `/user/forgot-password` | `/{authSiteSlug}/forgot-password-web` |
| `/user/reset-password/{token}` | `/{authSiteSlug}/reset-password-web?token=...&email=...` |
| `/user/email/verify` | `/{authSiteSlug}/email-verify` |
Route redirects ditakrifkan di `routes/web.php` dalam `Route::prefix('user')` group.
```php
Route::prefix('user')->group(function () {
Route::get('/register', function () {
return redirect('/' . SettingHelper::authSiteSlug() . '/register-web');
})->name('user.register');
// ...
});
```
---
## Controllers (POST Logic)
Controller masih digunakan untuk handle form submission (POST) sahaja:
| Controller | Method | Route | Fungsi |
|---|---|---|---|
| `LoginController` | `loginSubmit` | `POST /user/login` | Login authentication |
| `LoginController` | `logout` | `POST /user/logout` | Logout user guard only |
| `LoginController` | `sendResetLink` | `POST /user/forgot-password` | Send reset link email |
| `LoginController` | `resetPassword` | `POST /user/reset-password` | Reset password |
| `RegisterController` | `registerSubmit` | `POST /user/register` | Register user |
| `VerifyEmailController` | `resend` | `POST /user/email/verification-notification` | Resend verification email |
### Key Changes in Controllers
**LoginController::logout:**
```php
public function logout(Request $request)
{
Auth::guard('user')->logout();
$request->session()->regenerateToken();
// NOT invalidate() — to preserve admin backend session
return redirect('/' . SettingHelper::authSiteSlug() . '/login-web');
}
```
**LoginController::loginSubmit & resetPassword:**
```php
return redirect('/' . SettingHelper::authSiteSlug() . '/login-web');
```
**RegisterController::registerSubmit:**
```php
$request->merge(['site_slug' => $request->input('site_slug')]);
session()->put('auth_site_slug', $request->input('site_slug'));
return redirect('/' . SettingHelper::authSiteSlug() . '/email-verify');
```
**VerifyEmailController:**
```php
protected function redirectAfterVerify()
{
return '/' . SettingHelper::authSiteSlug() . '/login-web';
}
```
---
## User Dashboard
Page `user-dashboard` adalah destinasi selepas login berjaya. Mengandungi:
- Info user (username, email, name, member since, email verification status)
- Button Sign Out
- Auth protection (redirect ke login-web jika belum login)
### Entry Script (`$_shared` approach)
Entry script menggunakan object `$_shared` untuk pass data ke layout:
```blade
@php
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Exceptions\HttpResponseException;
if (!Auth::guard('user')->check()) {
throw new HttpResponseException(redirect()->to('/' . request()->segment(1) . '/login-web'));
}
$_shared->user = Auth::guard('user')->user();
@endphp
```
### Layout
Guna `$_shared->user` untuk access user data:
```blade
<div class="avatar">{{ strtoupper(substr($_shared->user->first_name, 0, 1)) }}</div>
<h2>Welcome, {{ $_shared->user->first_name }}!</h2>
<form method="POST" action="">
<input type="hidden" name="_logout" value="1">
<button type="submit">Sign Out</button>
</form>
```
### Login Redirect
Dalam `login-web` page layout, redirect selepas login berjaya:
```php
$redirect = "/" . request()->segment(1) . "/user-dashboard";
throw new HttpResponseException(redirect()->to($redirect));
```
---
## $_shared Object
`$_shared` adalah stdClass object yang disediakan oleh PortalHandler untuk berkongsi data antara Entry Script, Components, Page Content, dan Layout.
### Cara Kerja
```
PortalHandler:
$shared = new \stdClass();
$vars['_shared'] = $shared;
Entry Script:
$_shared->user = Auth::guard('user')->user();
↓ (object pass by reference — semua komponen guna object sama)
Layout:
{{ $_shared->user->name }}
```
### Kenapa Guna $_shared?
Entry script jalan dalam closure berasingan di PortalHandler. Variable PHP biasa (`$user`) mungkin tak sampai ke layout disebabkan cara Blade compile. `$_shared` adalah object — pass by reference — jadi apa-apa property yang set dalam entry script automatik nampak di layout.
### Peraturan
1. **Guna `@php ... @endphp`** untuk PHP code (atau `<?php ... ?>` jika prefer — kedua-dua berfungsi)
2. **Guna `$_shared->key = value`** untuk set data — bukan `$key = value`
3. **Guna `$_shared->key`** dalam Layout untuk access data
4. **Jangan guna variable biasa** — `$user` tak akan sampai ke layout
### Contoh Lengkap
**Entry Script:**
```blade
@php
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Exceptions\HttpResponseException;
if (!Auth::guard('user')->check()) {
throw new HttpResponseException(redirect()->to('/' . request()->segment(1) . '/login-web'));
}
$_shared->user = Auth::guard('user')->user();
$_shared->pageTitle = 'User Dashboard';
@endphp
```
**Layout:**
```blade
<h1>{{ $_shared->pageTitle }}</h1>
<p>Welcome, {{ $_shared->user->name }}</p>
```
---
## Email Notifications
### Password Reset Email
Di override dalam `FrontendUser@sendPasswordResetNotification` — generate URL ke CMS page menggunakan `SettingHelper::defaultSiteSlug()`:
```php
public function sendPasswordResetNotification($token)
{
$slug = SettingHelper::defaultSiteSlug();
ResetPassword::createUrlUsing(function ($notifiable, $token) use ($slug) {
return url('/' . $slug . '/reset-password-web?token='
. $token . '&email=' . urlencode($notifiable->email));
});
$this->notify(new ResetPassword($token));
}
```
### Email Verification
Di override dalam `FrontendUser@sendEmailVerificationNotification` — generate URL ke CMS page:
```php
public function sendEmailVerificationNotification()
{
$slug = SettingHelper::defaultSiteSlug();
VerifyEmail::createUrlUsing(function ($notifiable) use ($slug) {
return url('/' . $slug . '/email-verify-confirm?id='
. $notifiable->getKey() . '&hash='
. sha1($notifiable->getEmailForVerification()));
});
$this->notify(new VerifyEmail());
}
```
> **Nota:** Email notifications guna `defaultSiteSlug()` (bukan `authSiteSlug()`) kerana ia jalan dalam queue/notification context — tiada HTTP request untuk dapatkan site slug.
---
## Session Handling
### Guard Configuration (`config/auth.php`)
```php
'defaults' => [
'guard' => 'user',
'passwords' => 'user',
],
'guards' => [
'user' => ['driver' => 'session', 'provider' => 'frontend_users'],
'admin' => ['driver' => 'session', 'provider' => 'backend_users'],
],
'providers' => [
'frontend_users' => ['driver' => 'eloquent', 'model' => FrontendUser::class],
'backend_users' => ['driver' => 'eloquent', 'model' => BackendUser::class],
],
```
### Session Conflict Fix
Guna `session()->regenerateToken()` (bukan `session()->invalidate()`) untuk elakkan session ID berubah — supaya user guard dan admin guard boleh aktif serentak tanpa saling logout.
Dalam CMS page direct @php, guna manual login:
```php
$guard = Auth::guard('user');
session()->put($guard->getName(), $user->getAuthIdentifier());
$guard->setUser($user);
session()->regenerateToken();
```
---
## Password Reset Throttle
| Setting | Location | Default |
|---|---|---|
| Throttle duration | `config/auth.php:70` | 60 saat |
| Error message | `vendor/laravel/.../passwords.php` | "Please wait before retrying." |
---
## PortalHandler Additions
File: `app/Services/PortalHandler.php`
### `$_shared` Object
```php
$shared = new \stdClass();
$vars['_shared'] = $shared;
```
### `$errors` Variable
`ViewErrorBag` ditambah ke `$layoutVars` supaya `@error`, `$errors->any()`, `$errors->first()` boleh guna dalam mana-mana CMS page layout.
```php
$layoutVars = array_merge($vars, [
'__header__' => $header,
'__page__' => $pageHtml,
'__footer__' => $footer,
'errors' => session('errors') ?: new ViewErrorBag,
]);
```
### `HttpResponseException` Catch
Catch `HttpResponseException` sebelum `\Throwable` untuk allow redirect dari dalam CMS page layout.
```php
} catch (HttpResponseException $e) {
throw $e;
} catch (\Throwable $e) {
// show error
}
```
---
## Redirect Pattern
Untuk redirect selepas success dalam Web approach, guna `HttpResponseException` — bukannya `header()` + `exit`. Ini penting untuk pastikan session tersimpan dengan betul.
```php
throw new HttpResponseException(redirect()->to($redirect));
```
PortalHandler dah diubah suai untuk re-throw `HttpResponseException`.
> **Entry Script syntax:** Boleh guna sama ada `@php ... @endphp` (Blade) atau `<?php ... ?>` (PHP native) — kedua-dua berfungsi melalui `Blade::compileString()`. Pilih yang paling selesa dengan editor masing-masing.
---
## Ringkasan Fail
| Fail | Fungsi |
|---|---|
| `routes/web.php` | Route definitions + redirect closures (guna `SettingHelper::authSiteSlug()`) |
| `app/Models/Backend/FrontendUser.php` | Override email notifications (guna `SettingHelper::defaultSiteSlug()`) |
| `app/Http/Controllers/Frontend/Auth/LoginController.php` | POST login/logout/reset logic |
| `app/Http/Controllers/Frontend/Auth/RegisterController.php` | POST register logic (accept `site_slug`) |
| `app/Http/Controllers/Frontend/Auth/VerifyEmailController.php` | POST resend verification |
| `app/Services/PortalHandler.php` | CMS page renderer (`$_shared`, `$errors`, `HttpResponseException`) |
| `app/Http/Helpers/SettingHelper.php` | `authSiteSlug()` + `defaultSiteSlug()` helpers |
| `config/auth.php` | Guard, provider, password config |
| `resources/views/vendor/mail/html/header.blade.php` | Email template logo override |
---
## Cara Tambah Site Baru
1. Buat site baru kat Admin → CMS Builder → Sites
2. Create CMS pages yang diperlukan dalam site tu:
- Web approach: login-web, register-web, forgot-password-web, reset-password-web, email-verify, email-verify-confirm
- Controller approach: login, register (tambah hidden `site_slug` field)
3. Create user-dashboard page untuk post-login redirect
4. Route redirect automatik guna `SettingHelper::authSiteSlug()` — tak perlu setting apa-apa
5. Pastikan slug page layout guna `request()->segment(1)` untuk dynamic site links
6. Email notifications akan guna default site — set default site di Admin → CMS Builder → Sites