# 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
