Initial commit: existing codebase state

This commit is contained in:
root
2026-05-28 16:25:22 +08:00
commit b63cb6a3e8
16670 changed files with 2746770 additions and 0 deletions

View File

@@ -0,0 +1,244 @@
<!-- refreshed: 2026-05-28 -->
# Architecture
**Analysis Date:** 2026-05-28
## System Overview
This is a **Laravel 12 MVC** application — the **Sabo Integrated Debris Flow Monitoring and Early Warning System (SIDES)** for Sungai Kupang, Baling, Malaysia. It monitors rainfall, water level, and siren station data from debris flow monitoring stations, serves a dashboard with Leaflet maps + Chart.js graphs, sends Firebase Cloud Messaging (FCM) alerts, and exports data to Excel/PDF.
```text
┌─────────────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ Blade Layouts (layout.app, layout.homeapp) / PDF Templates / JSON │
│ `resources/views/layout/`, `resources/views/pdf/` │
├──────────────────┬────────────────────┬─────────────────────────────┤
│ Web Routes │ API Routes │ Console / Artisan │
│ `routes/web.php`│ `routes/api.php` │ `routes/console.php` │
├─────────┬────────┴─────────┬──────────┴──────────┬──────────────────┤
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │Controllers │ │ Auth Ctrlrs │ │ API Controllers │ │
│ │ Web │ │ (Breeze) │ │ (JSON responses) │ │
│ └──────┬─────┘ └──────┬───────┘ └────────┬──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ FcmService (`app/Services/FcmService.php`) │ │
│ │ → Firebase Cloud Messaging (topic-based push) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Eloquent Model Layer (minimal — only User model used) │ │
│ │ All other data access via DB::table() / DB::select() │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL Database │ │
│ │ Tables: users, station, rainfall, waterlevel, siren, │ │
│ │ notification, password_reset_tokens, sessions, │ │
│ │ cache, jobs, job_batches │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
## Component Responsibilities
| Component | Responsibility | File(s) |
|-----------|----------------|---------|
| Web Controllers | Serve Blade views with paginated/aggregated station data | `app/Http/Controllers/{Map,Rainfall,WaterLevel,Siren,Notification,cctv,Admin,Profile,Locale}Controller.php` |
| Auth Controllers | Login/Register/Password Reset/Verify Email via Laravel Breeze | `app/Http/Controllers/Auth/*.php` (9 files) |
| API Controllers | Return JSON for external IoT/second-screen consumption | `app/Http/Controllers/Api/{Station,Auth,Alert}Controller.php` |
| MapController | Dashboard entry — joins station + latest rainfall + latest waterlevel + latest siren | `app/Http/Controllers/MapController.php` |
| AdminController | CRUD for stations and users (access_level=1 gate) | `app/Http/Controllers/AdminController.php` |
| FcmService | Google OAuth2 token fetch → FCM v1 HTTP API topic push | `app/Services/FcmService.php` |
| Export Layer | Excel (Maatwebsite) + PDF (DomPDF) generation | `app/Exports/*.php`, `resources/views/pdf/*.blade.php` |
| Middleware | Authentication gate + admin role check + locale | `app/Http/Middleware/{Admin,Localization}Middleware.php` |
## Pattern Overview
**Overall:** Model-View-Controller with heavy use of raw SQL (PostgreSQL dialect) and **zero Eloquent ORM outside the User model**.
**Key Characteristics:**
- All database queries use `DB::table()` or `DB::select()` — not Eloquent models
- Only `User` model (`app/Models/User.php`) extends `Authenticatable` — used for auth + password reset notification override
- Controllers return Blade views (`view()`) for web, JSON (`response()->json()`) for API
- No repository/DAO abstraction layer — queries live directly in controllers
- Single service class (`FcmService`) handles Firebase Cloud Messaging
- Localization via session-based middleware with two locales: English (`en`) and Bahasa Malaysia (`bm`)
## Layers
**Web Controllers (Presentation + Logic):**
- Purpose: Handle HTTP requests, query data via raw SQL, return Blade views or JSON
- Location: `app/Http/Controllers/` (11 files) + `app/Http/Controllers/Auth/` (9 files)
- Contains: Request handling, SQL queries, view/JSON responses, form validation (inline `$request->validate()`)
- Depends on: `DB` facade, `Carbon`, `Excel`/`Pdf` facades, `FcmService` (AlertController only)
- Used by: Web routes (`routes/web.php`, `routes/auth.php`)
**API Controllers:**
- Purpose: External data endpoints for mobile/IoT clients, always return JSON
- Location: `app/Http/Controllers/Api/` (3 files)
- Contains: Station data queries, custom auth (username/password via raw SQL), FCM alert proxy
- Used by: API routes (`routes/api.php`)
- Key difference from web controllers: No middleware/auth gates on most API routes (except login)
**Service Layer:**
- Purpose: Encapsulate external service integrations
- Location: `app/Services/FcmService.php`
- Contains: Firebase OAuth2 token generation, FCM v1 HTTP API calls
- Dependencies: `google/auth` package, Laravel `Http` facade
**Export Layer:**
- Purpose: Generate downloadable files (Excel, PDF)
- Location: `app/Exports/*.php` + `resources/views/pdf/*.blade.php`
- Contains: Excel exports using `Maatwebsite\Excel` with `FromCollection`, `WithHeadings`, `ShouldAutoSize`; PDF exports using `Barryvdh\DomPDF`
- Patterns: Export classes receive station/date parameters via constructor, queries data in `collection()`, defines headings in `headings()`
**View Layer:**
- Purpose: Render HTML and PDF output
- Location: `resources/views/`
- Contains: Blade layouts (`layout.app`, `layout.homeapp`), section-based views, component templates, PDF templates
- Organization: Layouts in `layout/`, auth screens in `auth/`, nav in `nav/`, shared components in `components/`
- Key inheritance: `layout.homeapp``layout.home` (public); `layout.app` → all authenticated pages
## Data Flow
### Primary Request Path — Dashboard Page
1. Request hits `GET /dashboard` or `GET /` — mapped in `routes/web.php:60-61`
2. `MapController::getCurrentData()` `app/Http/Controllers/MapController.php:23`
3. Executes a raw query joining `station` with latest `rainfall`, `waterlevel`, `siren` records using subqueries
4. Returns `view('layout.dashboard', compact('data'))`
5. Blade view extends `layout.app` which includes `nav.header` (head, CSS, Leaflet), `nav.navbar` (navigation), and sections `content1` (station data table + map) and `content2` (info section)
6. Response includes embedded JavaScript with `window.translations` for i18n and Leaflet map rendering
### Auth Flow — Login
1. `POST /login``AuthenticatedSessionController::store()` `app/Http/Controllers/Auth/AuthenticatedSessionController.php:55`
2. Uses `LoginRequest` `app/Http/Requests/Auth/LoginRequest.php` for rate limiting (5 attempts)
3. Custom pre-check: queries `User::where('name', $name)` checks `is_blocked` flag
4. If blocked → returns error; if `login_attempts >= 3` → auto-block on failed attempt
5. If success → reset `login_attempts`, regenerate session, redirect to `/`
6. Login page (GET /login) also renders the dashboard map with station data (`create()` method)
### Alert Flow — External IoT Alert → FCM Push
1. External system `POST /api/alert``api.php:18``AlertController::send()` `app/Http/Controllers/Api/AlertController.php:17`
2. Receives `stationid`, `level`, `stationtype` (1=Rainfall, 2=WaterLevel, 3=Siren)
3. Injects `FcmService` via constructor
4. Calls `$this->fcm->sendToTopic($topic, $title, $body)`
5. `FcmService::sendToTopic()` `app/Services/FcmService.php:23`:
- Reads `FIREBASE_PROJECT_ID` and `FIREBASE_CREDENTIALS` from env
- Loads service account JSON, fetches OAuth2 token via `google/auth`
- POSTs to `https://fcm.googleapis.com/v1/projects/{projectId}/messages:send`
- Returns HTTP status code
### Export Flow — Download Excel
1. `GET /rainfall/historical/export``RainfallController::exportHourlyRainfallExcel()` `app/Http/Controllers/RainfallController.php:360`
2. Reads query params (`station`, `startdate`, `enddate`)
3. Instantiates `HourlyRainfallExport($stationid, $startDate, $endDate)` `app/Exports/HourlyRainfallExport.php`
4. Export class queries PostgreSQL with pivot (24 hour columns) and returns Excel download
### Export Flow — Download PDF
1. `GET /export/rainfall-history/pdf``NotificationController::exportHistoryRfPDF()` `app/Http/Controllers/NotificationController.php:91`
2. Queries `notification` join `station` where `stationtype=1`
3. `Pdf::loadView('pdf.rfhistory', compact('rfHistory'))` → renders `resources/views/pdf/rfhistory.blade.php`
4. Returns PDF download response
**State Management:**
- Session-based auth state (standard Laravel session driver)
- Locale stored in session (`Session::put('locale', ...)`)
- No client-side state management (Alpine.js for minimal interactivity in navigation dropdowns)
## Key Abstractions
**Controller Base Class:**
- Purpose: Empty abstract class for controller type-hinting
- File: `app/Http/Controllers/Controller.php`
- Pattern: `abstract class Controller {}` — no shared logic
**Form Request Classes:**
- Purpose: Encapsulate validation and auth logic for form submissions
- Files: `app/Http/Requests/ProfileUpdateRequest.php`, `app/Http/Requests/Auth/LoginRequest.php`
- Pattern: Extends `FormRequest`, defines `rules()`, may include `authorize()` and custom `authenticate()` methods
**Export Classes (Excel):**
- Purpose: Query + format data for spreadsheet download
- Files: `app/Exports/HourlyRainfallExport.php`, `app/Exports/WaterLevelExport.php`
- Pattern: Implement `FromCollection`, `WithHeadings`, `ShouldAutoSize`; constructor receives filter params; `collection()` returns query result
**View Components:**
- Purpose: Blade component classes for layout rendering
- Files: `app/View/Components/AppLayout.php`, `app/View/Components/GuestLayout.php`
- Pattern: Extends `Component`, returns view in `render()`
## Entry Points
**Web Application:**
- Location: `routes/web.php` (main), `routes/auth.php` (auth routes, included from web.php:85)
- Triggers: Browser HTTP requests
- Middleware stack: `web` group → `auth` or `guest` or `admin` → controller
- Custom middleware: `admin` alias → `AdminMiddleware`; `LocalizationMiddleware` appended to `web` group
**API:**
- Location: `routes/api.php`
- Triggers: External IoT systems, mobile clients
- Middleware stack: `api` group only (no auth middleware on station data endpoints)
- No authentication required on station data endpoints (public JSON)
**Console:**
- Location: `routes/console.php`
- Triggers: `php artisan inspire` (only default, no custom commands)
## Architectural Constraints
- **No Eloquent ORM usage:** All data access (except User auth) uses raw SQL via `DB::table()` or `DB::select()` with named/positional bindings. This means: no model relationships, no scopes, no eager loading, no model events.
- **PostgreSQL-specific SQL:** The codebase uses `TO_CHAR()`, `EXTRACT()`, `::date` casts, `::time` casts, `DATE_TRUNC()`, `INTERVAL`, `DISTINCT ON` — this is **not portable** to other databases.
- **SQL injection risk from string interpolation:** Several controllers use string interpolation (`"WHERE s.stationid = '{$stationFilter}'"`) instead of parameterized bindings (see `RainfallController.php:36`, `WaterLevelController.php:30`).
- **No automated tests for domain controllers:** Tests only exist for Laravel Breeze auth scaffolding (`tests/Feature/Auth/` — most files are empty).
- **Service layer is thin:** Only one service class (`FcmService`) exists. All business logic lives in controllers.
- **Mixed view inheritance:** `layout.app` uses `@include()` for header and `@yield()` for content sections; `layouts.app` (Laravel Breeze default) uses component-based slots (`{{ $slot }}`). These are two separate layout systems.
## Error Handling
**Strategy:** Per-controller try/catch with `redirect()->back()->with('error', $message)` pattern.
**Patterns:**
- `AdminController::storeUser()`: Explicit `try/catch` for `ValidationException` and general `\Exception` — returns first error message to session flash
- `AuthenticatedSessionController::store()`: Returns `back()->with('error', ...)` for block/failed states
- `FcmService`: Throws `\Exception` if token fetch fails — caught by `AlertController` implicitly (no try/catch)
- Validation on form requests: Uses `$request->validate()` inline in controllers (most cases) or dedicated `FormRequest` classes
## Cross-Cutting Concerns
**Logging:**
- No application-level logging detected beyond default Laravel log channel
- `FIREBASE_CREDENTIALS` JSON read from file, no credential caching
**Validation:**
- Inline `$request->validate([...])` in most controllers (`AdminController`, `RainfallController`, `WaterLevelController`)
- Form request classes for profile update (`ProfileUpdateRequest`) and login (`LoginRequest`)
- No validation on API POST `/alert` endpoint (raw `$request->stationid` access without validation)
**Authentication:**
- Web: Session-based with Laravel Breeze scaffolding, custom `is_blocked` check and `login_attempts` tracking
- API: Custom username/password verification via raw SQL + `Hash::check()` — returns JSON with `error` boolean
- Admin: Middleware gate `AdminMiddleware` checks `access_level !== 1`
- No token-based or OAuth API auth
**Localization:**
- Two locales: `en` (English) and `bm` (Bahasa Malaysia)
- Language files: `lang/{en,bm}/{messages,toast,auth,validation,pagination,passwords}.php`
- Route: `GET /locale/{locale}``LocaleController::setLocale()` — sets `App::setLocale()` and `Session::put('locale')`
- Middleware: `LocalizationMiddleware` reads locale from session on every request and sets `App::setLocale()`
---
*Architecture analysis: 2026-05-28*

View File

@@ -0,0 +1,527 @@
# Codebase Concerns
**Analysis Date:** 2026-05-28
## Tech Debt
### Widespread Raw SQL with String Interpolation Instead of Query Builder
**Issue:** The codebase predominantly uses `DB::select()` with raw SQL strings instead of Laravel's query builder or Eloquent. Many queries build conditions via string concatenation (`'{$userInput}'`), creating both SQL injection vulnerabilities and poor maintainability.
**Files:**
- `src/app/Http/Controllers/RainfallController.php:36-37``$stationCondition = " AND s.stationid = '{$stationFilter}'"` directly interpolated into raw SQL
- `src/app/Http/Controllers/RainfallController.php:39-90` — Entire query built via string concatenation with user-supplied dates and filters
- `src/app/Http/Controllers/RainfallController.php:102-191``rainfallSum()` method builds SQL with string concatenation for `$stationFilter` and `$dateFilter`
- `src/app/Http/Controllers/WaterLevelController.php:30,37-38` — String interpolation: `$stationCondition = " WHERE s.stationid = '{$stationFilter}' "` and `$dateCondition = " AND w.datetime = '{$sqlDate}' "`
- `src/app/Http/Controllers/WaterLevelController.php:49-57` — Raw SQL with interpolated variables
- `src/app/Http/Controllers/NotificationController.php:17-24,32-39,47-61` — Raw SQL with date literals
**Impact:** SQL injection risk (see Security section), code that is hard to refactor/test, and PostgreSQL-specific syntax that prevents database portability.
**Fix approach:** Replace all raw `DB::select("SELECT ... WHERE col = '$input'")` patterns with the query builder (e.g., `DB::table(...)->where('col', $input)->...`) or parameterized queries (`DB::select("... WHERE col = ?", [$input])`).
### Missing Eloquent Models for Core Tables
**Issue:** Only `src/app/Models/User.php` exists as an Eloquent model. The tables `station`, `rainfall`, `waterlevel`, `siren`, and `notification` are queried exclusively via `DB::table()` or `DB::select()` raw SQL. There are no relationships defined, no accessors/mutators, and no model-level concerns like casting or scopes.
**Files:**
- `src/app/Models/User.php` — Only model in the `app/Models/` directory
- All controllers use `DB::table('station')`, `DB::table('rainfall')`, etc.
**Impact:** Business logic is scattered across controllers. Adding columns, changing types, or adding accessors requires touching all query locations. No ability to use Eloquent relationships, eager loading, or model events.
**Fix approach:** Create models (`Station`, `Rainfall`, `WaterLevel`, `Siren`, `Notification`) with proper relationships, casts, and scopes. Migrate raw queries to Eloquent/query builder progressively.
### Duplicated Dashboard Query Across Three Controllers
**Issue:** The same complex query (stations left-joined with latest rainfall, waterlevel, siren readings via correlated subqueries) is duplicated verbatim in three separate controllers:
**Files:**
- `src/app/Http/Controllers/MapController.php:25-53``getCurrentData()`
- `src/app/Http/Controllers/Api/StationController.php:13-43``getCurrentData()`
- `src/app/Http/Controllers/Auth/AuthenticatedSessionController.php:20-48``create()` (login page)
**Impact:** Any schema change to the query requires three coordinated updates. Performance tuning must be done in three places. Increases maintenance burden.
**Fix approach:** Extract this query into a dedicated service class or repository method (e.g., `StationService::getLatestReadings()`) that can be called from all three controllers.
### Large Commented-Out Code Blocks
**Issue:** Multiple files contain large commented-out code blocks that serve no purpose:
**Files:**
- `src/app/Http/Controllers/cctvController.php:13-26` — Entire block of commented-out SQL
- `src/app/Http/Controllers/RainfallController.php:210-222` — Alternative query for rainfall graph data
- `src/app/Http/Controllers/RainfallController.php:285-287` — Commented station lookup
- `src/app/Providers/AppServiceProvider.php:23``URL::forceScheme('https')` commented out
- `src/app/Http/Controllers/Api/AlertController.php:23-28` — Alternative topic logic commented out
- `src/resources/views/layout/homeapp.blade.php:49-67` — Auth section entirely commented out
- `src/resources/views/layout/home.blade.php:9-10` — Commented Google Maps iframe
- `src/resources/views/layout/admin/usermgmt.blade.php:62,75,254-256,276-278` — Multiple commented out columns and pagination items
- `src/resources/views/layout/threshold.blade.php:153-158,178` — Commented out graph link and hardcoded example row
**Impact:** Codebase bloat. Confusing for new developers. Suggests incomplete refactoring.
**Fix approach:** Remove all dead code. Use version control history for reference.
### Inconsistent File and Class Naming
**Issue:** Several naming conventions violate PSR-4 and Laravel conventions:
**Files:**
- `src/app/Http/Controllers/cctvController.php` — Should be `CctvController.php` (PascalCase)
- `src/database/migrations/2025_11_06_072709_create_rainfall__table.php` — Double underscore in filename
- `src/database/migrations/2025_11_07_031601_create_siren__table.php` — Double underscore in filename
**Impact:** Minor inconsistency, but violates PSR standards. Can cause issues on case-sensitive filesystems and with some tooling.
### Placeholder/Lorem Ipsum Content
**Issue:** The landing page (`home.blade.php`) contains Lorem Ipsum placeholder text and generic "Card title" content:
**Files:**
- `src/resources/views/layout/home.blade.php:45-67` — Lorem Ipsum text and placeholder card titles
**Impact:** Unprofessional appearance if this page is publicly visible.
**Fix approach:** Replace with actual content about the SIDES system.
## Security Considerations
### SQL Injection in RainfallController
**Risk:** User-supplied `$stationFilter` and `$dateFilter` parameters are directly interpolated into raw SQL strings without parameterization.
**Files:**
- `src/app/Http/Controllers/RainfallController.php:34-37``$stationCondition = " AND s.stationid = '{$stationFilter}'"` — raw interpolation of user input
- `src/app/Http/Controllers/RainfallController.php:46``CAST('$displayDate' AS timestamp)` — date from user input interpolated
- `src/app/Http/Controllers/RainfallController.php:69,75-81``CAST('$dateFilter' as date)` — user-supplied date, multiple times
- `src/app/Http/Controllers/RainfallController.php:134-136``$bindings` approach used in `rainfallSum()` is better but still has one raw `$sqlDate` on line 134-136
- `src/app/Http/Controllers/WaterLevelController.php:28-38` — String interpolation of `$stationFilter` and `$sqlDate` with no parameterization
**Current mitigation:** None for string-interpolated queries. The `rainfallSum()` method in `RainfallController` parametrizes via bindings (partially correct), but `index()` and `WaterLevelController::index()` do not.
**Recommendations:** Immediately convert all string interpolation patterns to parameterized queries using either the query builder or `DB::select($sql, $bindings)`. The most critical vulnerabilities are:
- `WaterLevelController.php:30``WHERE s.stationid = '{$stationFilter}'`
- `RainfallController.php:36``AND s.stationid = '{$stationFilter}'`
- `RainfallController.php:69``CAST('$dateFilter' as date)`
### Unauthenticated API Endpoint Triggers Firebase Notifications
**Risk:** The `/api/alert` POST endpoint (`src/app/Http/Controllers/Api/AlertController.php`) accepts any request body with `stationid`, `level`, and `stationtype` fields and sends Firebase Cloud Messaging notifications. There is no authentication, no rate limiting, and no validation on this endpoint.
**Files:**
- `src/app/Http/Controllers/Api/AlertController.php:17-51``send()` method processes arbitrary data without auth
- `src/routes/api.php:18``Route::post('/alert', [AlertController::class, 'send'])` — no middleware
**Current mitigation:** None.
**Recommendations:** Add API token authentication (e.g., Laravel Sanctum), input validation, rate limiting, and logging to this endpoint. An attacker could spam all subscribers with false flood alerts.
### API Endpoints Expose All Data Without Authentication
**Risk:** All `/api/station/*` endpoints (`src/routes/api.php:10-17`) are publicly accessible with no authentication. These expose detailed station data including IDs, names, coordinates, sensor readings, and alert levels.
**Files:**
- `src/app/Http/Controllers/Api/StationController.php` — All methods (`getCurrentData`, `getRainfallData`, `getWlData`, `getCurrentNoti`, `getHistory`, `getSiren`, `getSirenHistory`)
- `src/routes/api.php:10-16` — No middleware applied
**Current mitigation:** None. Maps already show station locations publicly on the dashboard.
**Recommendations:** At minimum, apply rate limiting. Consider API authentication if these endpoints should be restricted to authorized clients only.
### API Login Returns User Details Without Token
**Risk:** The `/api/login` endpoint (`src/app/Http/Controllers/Api/AuthController.php`) returns user `id`, `name`, `email`, and `acc_lvl` in plain JSON without issuing any session token or API key. There is no way to maintain session state in subsequent API calls.
**Files:**
- `src/app/Http/Controllers/Api/AuthController.php:12-56` — Custom auth that doesn't use Laravel's session or token auth systems
- `src/routes/api.php:17``Route::post('/login', [AuthController::class, 'login'])`
**Current mitigation:** None.
**Recommendations:** Integrate Laravel Sanctum for token-based API authentication. After login, issue a token that must be sent as a Bearer token on subsequent requests.
### XSS via Unescaped Blade Output in Station Management
**Risk:** The station management view uses `{!! !!}` (unescaped Blade output) to render station type badges. These badge values are built from user-controlled data stored in the database (`$row->rainfall`, `$row->waterlevel`, `$row->siren`).
**Files:**
- `src/resources/views/layout/admin/stationmgmt.blade.php:110``{!! $types ? implode(' ', $types) : '<span class="badge bg-secondary">No Type</span>' !!}`
**Current mitigation:** The badge values (`rainfall`, `waterlevel`, `siren`) are integers (0 or 1), so the risk is limited to the label text. However, other display data like `$row->name`, `$row->district`, etc. comes from user input (stored via `AdminController::storeStation()`). These are output elsewhere via `{{ }}` (escaped), but this one `{!! !!}` instance opens a vector if the underlying data is ever manipulated.
**Recommendations:** Replace `{!! $types ... !!}` with `{{ $types ... }}` and build the HTML span tags client-side or via safe Blade components. Always escape dynamic content.
### Password Policy Inconsistency
**Risk:** User passwords set via the admin panel (`AdminController::storeUser`, `updatePassword`) only require `min:6` characters, while user self-registration uses `Rules\Password::defaults()` (which enforces more stringent rules like mixed case, special characters, etc.).
**Files:**
- `src/app/Http/Controllers/AdminController.php:98``'password' => 'required|string|min:6|confirmed'`
- `src/app/Http/Controllers/AdminController.php:229``'password' => 'required|string|min:6|confirmed'`
- `src/app/Http/Controllers/Auth/RegisteredUserController.php:35``'password' => ['required', 'confirmed', Rules\Password::defaults()]`
**Current mitigation:** Both paths use `bcrypt()`/`Hash::make()` so passwords are hashed.
**Recommendations:** Standardize on `Rules\Password::defaults()` for all password creation paths.
### Inverted Block/Unblock Logic in User Management
**Risk:** The `updateUsers()` method in `AdminController` has inverted logic for the `is_blocked` checkbox. When the checkbox is present (checked), the user is set to `is_blocked = 0` (unblocked). When absent (unchecked), the user is set to `is_blocked = 1` (blocked). This creates a confusing UX where checking the "enable account" box actually means "unblock the user."
**Files:**
- `src/app/Http/Controllers/AdminController.php:177-192``$request->has('is_blocked')` sets `is_blocked = 0`, absence sets `is_blocked = 1`
- `src/resources/views/layout/admin/usermgmt.blade.php:131` — Checkbox field named `is_blocked` with misleading toggle
- `src/resources/views/layout/admin/usermgmt.blade.php:133` — Label reads `__('messages.block')` when unchecked, `__('messages.active')` when checked (inverted from what the name implies)
**Recommendations:** Rename the checkbox to something semantically clear like `unblock_account`. Fix the logic so checked = blocked status = true, unchecked = blocked = false. Or restructure to use a dedicated `block` and `unblock` endpoint.
### No CSRF Protection on Web Auth Routes (post-login)
**Risk:** While Laravel applies CSRF protection to web routes by default, the API routes have no CSRF protection, which is expected. However, the `AuthenticatedSessionController::store()` has custom authentication logic that bypasses the standard `LoginRequest::authenticate()` method. The controller first queries for the user manually on line 62 before calling `Auth::attempt()`.
**Files:**
- `src/app/Http/Controllers/Auth/AuthenticatedSessionController.php:55-97` — Custom login logic diverges from Breeze scaffolding
**Current mitigation:** Standard CSRF token is still present on the login form view (`src/resources/views/layout/app.blade.php:31` includes `@csrf`).
**Recommendations:** Keep CSRF protection on all web routes. The custom authentication logic should be reviewed for race conditions (user looks up is_blocked, then Auth::attempt runs).
### FCM Credentials Exceptions
**Risk:** `FcmService::__construct()` calls `file_get_contents()` on a path derived from `env('FIREBASE_CREDENTIALS')`. If the file doesn't exist or the path is misconfigured, this throws an uncaught `\Exception` that returns a 500 error.
**Files:**
- `src/app/Services/FcmService.php:17-20` — No error handling around file access
**Current mitigation:** None.
**Recommendations:** Wrap the file read in a try-catch, validate the file exists first, and provide a meaningful error response.
## Performance Bottlenecks
### Missing Database Indexes on Joined Columns
**Problem:** Several tables lack indexes on columns used extensively in JOINs and WHERE filters.
**Files:**
- `src/database/migrations/2025_11_06_072709_create_rainfall__table.php` — No index on `stationid` or `timestamp`
- `src/database/migrations/2025_11_06_072738_create_waterlevel_table.php` — No index on `stationid` or `datetime`
- `src/database/migrations/2025_11_07_024940_create_notification_table.php` — No index on `stationid`, `timestamp`, or `stationtype`
- `src/database/migrations/2025_11_07_031601_create_siren__table.php` — No index on `stationid` or `active_time`
**Cause:** All migrations use plain column definitions without `->index()`.
**Improvement path:** Add indexes on all foreign key and filter columns:
- `rainfall.stationid`, `rainfall.timestamp`
- `waterlevel.stationid`, `waterlevel.datetime`
- `notification.stationid`, `notification.timestamp`, `notification.stationtype`
- `siren.stationid`, `siren.active_time`
### Inefficient Correlated Subqueries in Dashboard Queries
**Problem:** The repeated dashboard query uses correlated subqueries (e.g., `SELECT MAX(timestamp) FROM rainfall WHERE stationid = s.stationid`) that execute once per station row in the outer query. For 50 stations, this executes 150+ subqueries (rainfall + waterlevel + siren × stations).
**Files:**
- `src/app/Http/Controllers/MapController.php:27-37` — Three correlated subqueries
- `src/app/Http/Controllers/Api/StationController.php:15-25` — Same pattern duplicated
- `src/app/Http/Controllers/Auth/AuthenticatedSessionController.php:22-32` — Same pattern tripled
**Cause:** PostgreSQL can optimize these with appropriate indexes, but without them, this is a full table scan × stations count.
**Improvement path:** Add indexes as above. Consider using window functions (`ROW_NUMBER() OVER (PARTITION BY stationid ORDER BY timestamp DESC)`) instead of correlated subqueries for potentially better performance with large datasets.
### Unpaginated Large Exports and API Endpoints
**Problem:** Multiple export methods and API endpoints load ALL matching records at once without pagination:
**Files:**
- `src/app/Http/Controllers/NotificationController.php:93-102``exportHistoryRfPDF()` — loads ALL notification history
- `src/app/Http/Controllers/NotificationController.php:108-119``exportHistoryWlPDF()` — loads ALL WL history
- `src/app/Http/Controllers/SirenController.php:51-58``exportHistorySirenPDF()` — loads ALL siren history
- `src/app/Http/Controllers/Api/StationController.php:43,64,86,106,129,147,161` — All API endpoints load unfiltered/complete datasets
**Cause:** No pagination or date-range limits implemented.
**Improvement path:** Add date range restrictions and/or chunked processing for exports. Add pagination (`->paginate()` or `->limit()`) to API endpoints. For PDFs, consider streaming or limiting to a maximum record count.
### Complex Window Functions Without Indexes
**Problem:** The `historicalRainfall()` method and `HourlyRainfallExport` use `DISTINCT ON` with window functions partitioned by `stationid` and `EXTRACT(HOUR FROM timestamp)`. Without proper indexes, this forces sequential scans.
**Files:**
- `src/app/Http/Controllers/RainfallController.php:322-348``latest_per_hour` CTE with `DISTINCT ON`
- `src/app/Exports/HourlyRainfallExport.php:35-54` — Same pattern duplicated for Excel export
**Improvement path:** Add composite index on `rainfall(stationid, timestamp)` to support the DISTINCT ON ordering.
## Reliability Concerns
### Missing Validation on Required Parameters
**Issue:** Several controller methods fail to validate that required parameters are provided before using them:
**Files:**
- `src/app/Http/Controllers/WaterLevelController.php:84-86``wlHistory()` doesn't validate `stationid` or `startDate` are present before calling `Carbon::parse($startDate)` — would cause an error if called without parameters
- `src/app/Http/Controllers/WaterLevelController.php:127-128``exportHistoricalWl()` same issue
- `src/app/Http/Controllers/RainfallController.php:295-299``historicalRainfall()` doesn't validate `startDate` or `endDate` before `Carbon::parse()`; will crash with `null`
- `src/app/Http/Controllers/RainfallController.php:361-364``exportHourlyRainfallExcel()` same issue
### Exception Messages Leaked to Users
**Issue:** Several controllers catch generic `\Exception` and pass `$e->getMessage()` directly to redirect back with error flash:
**Files:**
- `src/app/Http/Controllers/AdminController.php:122``return redirect()->back()->with('error', $e->getMessage())`
- `src/app/Http/Controllers/AdminController.php:214` — same pattern
- `src/app/Http/Controllers/AdminController.php:253` — same pattern
**Impact:** In production with `APP_DEBUG=false`, this still leaks internal exception messages to end users. These may contain SQL syntax errors, file paths, or other system information. In debug mode (`APP_DEBUG=true`), full stack traces would be shown.
**Fix approach:** Log the actual exception with `Log::error()`, and return a generic user-friendly message.
### Missing Transaction Safety for Write Operations
**Issue:** Multiple sequential database write operations are not wrapped in database transactions. A failure midway can leave data in an inconsistent state:
**Files:**
- `src/app/Http/Controllers/AdminController.php:196-204` — Updates `is_blocked` and `login_attempts` then separately updates `name`, `email`, `access_level` — two separate update queries, not transactional
- `src/app/Http/Controllers/AdminController.php:72-85``storeStation()` insert statement not wrapped in transaction
- `src/app/Http/Controllers/AdminController.php:102-111``storeUser()` insert with no transaction
**Fix approach:** Wrap write operations in `DB::transaction()`.
### Potential Memory Exhaustion from Large PDF Generation
**Issue:** The DomPDF exports load all matching records into memory via `->get()`, then pass the entire collection to DomPDF. For datasets with months of history records, this could exhaust PHP memory limits.
**Files:**
- `src/app/Http/Controllers/NotificationController.php:93-97` — Rainfall history PDF loads all records
- `src/app/Http/Controllers/NotificationController.php:108-113` — Water level history PDF loads all records
- `src/app/Http/Controllers/SirenController.php:51-58` — Siren history PDF loads all records
**Fix approach:** Add date range filtering (at least a 90-day cap), add pagination, or chunk the data and batch-generate PDF pages.
## Fragile Areas
### WaterLevelController String Interpolation Queries
**Files:** `src/app/Http/Controllers/WaterLevelController.php:28-57`
**Why fragile:** The entire method builds conditions via string concatenation with user input. Adding a new filter condition or changing the query logic requires careful string manipulation. Any missing space or quote in the concatenation breaks the query. Additionally, these SQL injection vectors mean a malicious input could drop tables or exfiltrate data.
**Safe modification:** Convert to query builder: `DB::table('station as s')->join('waterlevel as w', ...)->when($stationFilter, fn($q) => $q->where('s.stationid', $stationFilter))->when($dateFilter, fn($q) => $q->where('w.datetime', $dateFilter))`.
**Test coverage:** No tests exist for this controller (only example tests present).
### AuthenticatedSessionController Custom Login Logic
**Files:** `src/app/Http/Controllers/Auth/AuthenticatedSessionController.php:55-97`
**Why fragile:** The custom login flow manually queries the user, checks `is_blocked`, attempts auth, then manually manages `login_attempts` and `is_blocked` fields. This duplicates (and partially overrides) the standard `LoginRequest::authenticate()` behavior. The logic for incrementing `login_attempts` and blocking after 3 failures is a race condition — concurrent login attempts could each increment before the user is blocked, allowing more attempts than intended.
**Safe modification:** Move login attempt tracking and blocking logic into a proper authentication middleware or event listener. Use database transactions or atomic increments for login_attempts.
**Test coverage:** No test exists for the custom authentication flow.
### FcmService Credential Loading
**Files:** `src/app/Services/FcmService.php:15-21`
**Why fragile:** The constructor reads FIREBASE_CREDENTIALS from env, then calls `file_get_contents()` on the resolved path. If env is empty, the file is missing, or the JSON is malformed, this throws exceptions that propagate as 500 errors. The credentials are read on every request (service is instantiated per-request), causing unnecessary filesystem I/O.
**Safe modification:** Cache the credentials in memory (singleton service), validate the file exists before reading, and throw a specific exception with proper logging.
## Scaling Limits
### Database Table Growth for Sensor Readings
**Current capacity:** No limits defined. The `rainfall`, `waterlevel`, and `notification` tables collect time-series sensor data that grows indefinitely.
**Limit:** Without indexes and with correlated subqueries, performance degrades as these tables grow. The dashboard query scans the max timestamp for each station — with 50+ stations and years of data, this becomes slow.
**Scaling path:**
1. Add composite indexes on `(stationid, timestamp)` for rainfall, waterlevel, and notification tables
2. Implement data retention/archival policy (e.g., move data older than 1 year to archive tables)
3. Replace correlated subqueries with window functions or join against derived tables
4. Consider partitioning rainfall and waterlevel tables by month/year
### PDF and Excel Exports for Large Datasets
**Current capacity:** All exports load complete result sets into memory.
**Limit:** With tens of thousands of notification records, DomPDF and Laravel-Excel will exhaust PHP memory (typically 128MB-256MB container memory).
**Scaling path:** Add maximum date range limits (e.g., 90 days), add record count limits, or implement chunked/streamed export.
## Dependencies at Risk
### barryvdh/laravel-dompdf (^3.1)
**Risk:** DomPDF has known memory limitations for large HTML-to-PDF conversions. It also depends on the `Doctrine` XML parser and can produce inconsistent rendering.
**Impact:** Large exports (notification history) will fail silently or produce 500 errors.
**Migration plan:** Consider `laravel-snappy` (wkhtmltopdf wrapper) for better rendering, or a SaaS solution like PDF.co for high-volume exports. For now, add date range limits and memory limit increases.
### google/auth (^1.49)
**Risk:** The `google/auth` library is used directly (not via Laravel's Firebase package like `kreait/laravel-firebase`) in `FcmService.php`. This is a lower-level implementation that requires manual token management.
**Impact:** Relies on manual credential loading and token fetching. If Google's auth endpoints change, the implementation breaks. No config caching or token reuse.
**Migration plan:** Either switch to `kreait/laravel-firebase` (official Firebase PHP SDK for Laravel) or add token caching to the current implementation to reduce auth requests.
### maatwebsite/excel (^3.1)
**Risk:** Version 3.1 is stable but receives less frequent updates. The package wraps PhpSpreadsheet, which uses significant memory for large exports.
**Impact:** Large historical rainfall exports could exhaust memory.
**Migration plan:** For Excel exports, limit result sets to reasonable sizes. Consider streaming writes for large datasets.
## Missing Critical Features
### No API Rate Limiting
**Problem:** None of the API routes apply rate limiting. An attacker can hit any endpoint arbitrarily, including the unauthenticated `/api/alert` endpoint.
**Blocks:** Prevents abuse detection and mitigation.
### No API Authentication for Mobile/Third-Party Clients
**Problem:** The API has a `/api/login` endpoint that returns user info but no token. There's no mechanism to authenticate subsequent API requests. All data endpoints are effectively public.
**Blocks:** Restricting access to authorized mobile app users.
### No Audit Logging
**Problem:** There is no logging of admin actions — station creation, user creation, password changes, or data exports are not logged.
**Files:** All `AdminController` methods lack logging.
**Blocks:** Auditing who changed what and when for security compliance.
### No Health Check Endpoint
**Problem:** The Docker containers have no health checks, and the application has no health check endpoint.
**Blocks:** Orchestration tools (Docker, K8s) cannot verify application health.
### No Input Sanitization for CCTV Links
**Problem:** CCTV links (`cctv_link`) stored in the database are output directly as `href="http://{{$row->cctv_link}}"` in the CCTV view with a hardcoded `http://` prefix. Users can input any URL including `javascript:` URLs or phishing links.
**Files:**
- `src/app/Http/Controllers/AdminController.php:65``'cctv_link' => 'nullable|string|max:500'` — no URL validation
- `src/resources/views/layout/cctv.blade.php:31``<a href="http://{{$row->cctv_link}}" target="_blank">` — unvalidated URL with hardcoded `http://` prefix
- `src/resources/views/layout/admin/stationmgmt.blade.php:183` — CCTV link input accepts arbitrary text
**Impact:** An admin could paste a malicious or malformed URL. The hardcoded `http://` prefix means legitimate HTTPS links would be broken (e.g., `https://example.com` becomes `http://https://example.com`).
## Test Coverage Gaps
### Near-Zero Test Coverage
**What's not tested:**
- All API controllers (`StationController`, `AuthController`, `AlertController`) — no tests exist
- All web controllers (`RainfallController`, `WaterLevelController`, `SirenController`, `NotificationController`, `AdminController`, `MapController`, `cctvController`, `LocaleController`) — no tests exist
- `FcmService` — no tests exist
- All export classes (`HourlyRainfallExport`, `WaterLevelExport`) — no tests exist
- The custom authentication flow in `AuthenticatedSessionController` — no tests exist
- The AdminMiddleware — no tests exist
- The LocalizationMiddleware — no tests exist
**Files:** Only two example tests exist:
- `src/tests/Feature/ExampleTest.php` — Basic HTTP 200 check
- `src/tests/Unit/ExampleTest.php` — Asserts `true === true`
**Risk:** Any refactoring, especially of the SQL-heavy controllers, could introduce regressions with no safety net. The authentication system (block logic, login_attempts) is particularly risky.
**Priority:** High
## Configuration Concerns
### Missing .env.example
**Issue:** The `composer setup` script attempts to copy `.env.example` to `.env`, but no `.env.example` file exists in `src/`. This means the setup step fails silently and the application may run with default config values (unsafe defaults like empty database passwords).
**Files:**
- `src/composer.json:41``"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""`
**Impact:** Developers/ops cannot easily replicate the environment configuration.
### Default Session Encryption Disabled
**Issue:** `config/session.php` defaults `'encrypt' => env('SESSION_ENCRYPT', false)`. Session data is not encrypted by default.
**Impact:** If sessions are stored in the database (the default driver), session data is readable by anyone with database access.
### Default DB Password is Empty String
**Issue:** Multiple database connection configs default `password` to an empty string:
- `config/database.php:53` — MySQL: `'password' => env('DB_PASSWORD', '')`
- `config/database.php:73` — MariaDB: `'password' => env('DB_PASSWORD', '')`
- `config/database.php:93` — PostgreSQL: `'password' => env('DB_PASSWORD', '')`
- `config/database.php:108` — SQLSRV: `'password' => env('DB_PASSWORD', '')`
**Impact:** If the `.env` file is missing the `DB_PASSWORD` key, the database will be accessed with an empty password.
## Deployment Concerns
### Docker Setup Issues
**External network requirement:**
- `docker-compose.yml:8``external: true` for `sides_net`. The network must be manually created before `docker compose up`.
- **Fix:** Either remove `external: true` and let Docker Compose create the network, or document the required `docker network create sides_net` step.
**Double COPY without consistent ownership:**
- `Dockerfile:76``COPY ./src /var/www/html` (copies as root)
- `Dockerfile:79``COPY --chown=www:www ./src /var/www/html` (copies again as www)
- **Impact:** Files are copied twice, first as root then as www. The second copy should be the only one needed.
- **Fix:** Remove the first `COPY` line. Keep only the `--chown=www:www` version.
**PostgreSQL data directory mismatch:**
- `docker-compose.yml:29` — Volume mapped to `/var/lib/postgres/data` instead of default `/var/lib/postgresql/data`
- **Impact:** If PostgreSQL expects data in the default location, startup may fail or data won't persist.
- **Fix:** Use the standard PostgreSQL data directory `/var/lib/postgresql/data` or add an explicit `PGDATA` environment variable.
**No queue worker process:**
- The Dockerfile builds a PHP-FPM container but doesn't start a Laravel queue worker. The `QUEUE_CONNECTION` defaults to `database`, but there's no process actually processing jobs.
- **Impact:** Password reset emails and any queued notifications will never be sent.
- **Fix:** Add a supervisor configuration to run `php artisan queue:work` in the container.
**pgAdmin and Adminer exposed in production config:**
- `docker-compose.yml:54-67,70-79` — pgAdmin (port 5050) and Adminer (port 6060) are configured alongside the main services. If deployed to production, these are database management interfaces exposed on the network.
- **Impact:** Increased attack surface. Compromise of pgAdmin/Adminer gives direct database access.
- **Fix:** Separate infrastructure tools into a development-only docker-compose.override.yml file.
**Dozzle with shell and container actions enabled:**
- `docker-compose.yml:89-95``DOZZLE_ENABLE_ACTIONS=true` and `DOZZLE_ENABLE_SHELL=true` — allows anyone with access to Dozzle to execute commands in any container.
- **Impact:** Significant security risk in production environments.
- **Fix:** Disable shell and actions in production, or add authentication.
**Filebrowser runs as root:**
- `docker-compose.yml:103``user: "0:0"` — FileBrowser container runs as root with access to `/root/sides`.
- **Impact:** If FileBrowser is compromised, the attacker has root access to the mounted directories.
**No health checks on any service:**
- No `healthcheck` directive defined for `app`, `postgres`, or `web` services.
- **Impact:** Docker has no way to know if services are healthy. Dependencies (`depends_on`) only wait for container start, not service readiness.
### Nginx Security Headers Gaps
**Issue:** The Nginx config sets `X-Frame-Options`, `X-XSS-Protection`, and `X-Content-Type-Options` but is missing:
- `Strict-Transport-Security` (HSTS)
- `Content-Security-Policy` (CSP)
- `Referrer-Policy`
**Files:**
- `docker/nginx/default.conf:8-10` — Only three security headers set
**Impact:** The application is more susceptible to clickjacking (partially mitigated), MIME-type sniffing (mitigated), and lacks comprehensive CSP that could prevent XSS.
---
*Concerns audit: 2026-05-28*

View File

@@ -0,0 +1,377 @@
# Coding Conventions
**Analysis Date:** 2026-05-28
## Naming Patterns
**Controllers:**
- PascalCase singular nouns: `RainfallController`, `WaterLevelController`, `AdminController`, `SirenController`, `MapController`, `LocaleController`, `ProfileController`, `cctvController`
- **Inconsistency:** `cctvController.php` is lowercase `c`, should be `CctvController.php`
- API controllers namespaced under `App\Http\Controllers\Api\`: `StationController`, `AuthController`, `AlertController`
- Auth controllers namespaced under `App\Http\Controllers\Auth\`: `AuthenticatedSessionController`, `RegisteredUserController`, `PasswordController`, `NewPasswordController`, `PasswordResetLinkController`, `ConfirmablePasswordController`, `EmailVerificationPromptController`, `EmailVerificationNotificationController`, `VerifyEmailController`
**Controller Methods:**
- camelCase, descriptive verbs: `index()`, `storeStation()`, `updateStation()`, `deleteStation()`, `storeUser()`, `updateUsers()`, `updatePassword()`, `deleteUser()`, `stationDisplay()`, `userDisplay()`, `rainfallGraph()`, `rainfallSum()`, `historicalRainfall()`, `graphData()`, `graphPage()`, `getStations()`, `getCurrentData()`, `exportHourlyRainfallExcel()`, `exportHistoricalWl()`, `SirenHistory()`, `rainfallNotification()`, `wlNotification()`, `sirenNotification()`, `rfHistory()`, `wlHistory()`
- **Inconsistency:** `SirenHistory()` starts with uppercase S; `stationDisplay()` inconsistent with rest (`stationDisplay` vs `displayStations`)
- Auth scaffold methods follow Laravel Breeze conventions: `create()`, `store()`, `edit()`, `update()`, `destroy()`, `show()`
**Models:**
- Only one model: `User.php``App\Models\User` extends `Authenticatable`
- Singular PascalCase: `User`
**Routes:**
- kebab-case URLs: `/rainfall/historical`, `/waterlevel/graph/{stationid}`, `/station/current`, `/station/rainfall`
- Snake-case named routes with dot-notation: `profile.edit`, `profile.update`, `profile.destroy`, `rainfall.graph`, `waterlevel.graph`, `historicalwl`, `historicalwlexport`, `stationmanagement.store`, `usermgmt.updatePassword`, `export.historysiren.pdf`
- Route parameters use camelCase: `{stationid}`, `{dates}`, `{userid}`, `{locale}`
- No resource route definitions — all routes manually defined
**Views/Directories:**
- Blade templates under `resources/views/layout/` organized by feature:
- `layout/rainfall.blade.php`, `layout/waterlevel.blade.php`, `layout/cctv.blade.php`
- `layout/admin/stationmgmt.blade.php`, `layout/admin/usermgmt.blade.php`
- `layout/notification/rainfall.blade.php`, `layout/notification/history/rainfall.blade.php`
- `layout/siren/home.blade.php`, `layout/siren/history.blade.php`
- `layout/graph/rainfall.blade.php`
- View path names match controller method return values: `view('layout.rainfall', ...)`, `view('layout.admin.stationmgmt', ...)`
- Views rendered via dot-notation: `layout.rainfall`, `layout.admin.stationmgmt`, `pdf.sirenhistory`, `auth.register`
**Database:**
- Table names: lowercase snake-case plural: `users`, `station`, `rainfall`, `waterlevel`, `siren`, `notification` (mixed — `station` is singular, `users` is plural)
- Migration filename: `YYYY_MM_DD_HHMMSS_create_{table}_table.php`
- Columns: snake_case: `stationid`, `access_level`, `login_attempts`, `is_blocked`, `cctv_link`, `mainriverbasin`, `subriverbasin`, `email_verified_at`, `remember_token`
- Index: `$table->id()`, `$table->string('name')`, `$table->timestamps()`
## Code Style
**PSR-12 Compliance:**
- Namespace declarations: `namespace App\Http\Controllers;`
- Class brace on new line
- Method brace on new line
- `<?php` opening tag on line 1
- Use statements grouped by namespace
**Indentation Issues:**
- Mix of consistent 4-space indentation in most files
- Some files have inconsistent indentation: `RainfallController.php` lines 26, 92-94, 209-222
- `WaterLevelController.php` has extra blank lines and inconsistent spacing
- `AdminController.php` try/catch blocks have widely varying indentation depth
**Line Length:**
- Some SQL strings in controllers exceed 120 characters (e.g., `RainfallController.php` line 39-90, lines 161-174)
- No line-length enforcement visible; no `.editorconfig` or `.php-cs-fixer` config detected
**Semicolons:**
- Consistently used — no omissions detected
**Cast/Type Declarations:**
- Controllers use PHP 8+ typed return values only in ProfileController: `public function edit(Request $request): View`
- Breeze-generated auth controllers consistently use return type hints: `: View`, `: RedirectResponse`, `: RedirectResponse|View`
- Custom controllers (Rainfall, WaterLevel, Siren, etc.) omit return type declarations entirely
- `User` model uses typed property declaration: `protected function casts(): array`
**Strict Types:**
- No `declare(strict_types=1)` in any file — strict types not used
## Import Organization
**Order:**
1. `Illuminate` / Laravel facades and classes (alphabetical by full path)
2. `App` local namespaces
3. Third-party packages (Carbon, Maatwebsite, Barryvdh, Google\Auth)
4. No blank-line grouping — imports are listed in a single flat block
**Examples:**
```php
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
```
**Path Aliases:**
- No custom `use` path aliases detected — only default namespace imports
- Fully qualified class names used in routes: `[App\Http\Controllers\RainfallController::class, 'index']`
- `use App\Exports\HourlyRainfallExport;` and `use Maatwebsite\Excel\Facades\Excel;` are separated with comment blocks explaining why
## Error Handling
**Patterns:**
1. **Redirect back with flash messages** (most common):
```php
return redirect()->back()->with('success', __('toast.stationsuccess'));
return redirect('/dashboard')->with('error', 'Unauthorized Access');
```
2. **Try/catch with ValidationException handling** (AdminController):
```php
try {
$validated = $request->validate([...]);
DB::table('users')->insert([...]);
return redirect()->back()->with('success', __('toast.usersuccess'));
} catch (ValidationException $e) {
$errorMessage = collect($e->errors())->flatten()->first();
return redirect()->back()->with('error', $errorMessage);
} catch (\Exception $e) {
return redirect()->back()->with('error', $e->getMessage());
}
```
- Catches `ValidationException` first, then generic `\Exception`
- Error messages extracted from first validation error
3. **Manual validation checks** (Api/AuthController):
```php
if (!$request->username || !$request->password) {
return response()->json([
'error' => true,
'message' => 'Required field are missing'
]);
}
```
4. **Early return for no data** (RainfallController `graphData`):
```php
if (!$latest) {
return response()->json([
'labels' => [],
'data' => []
]);
}
```
5. **Back with error array** (auth scaffold):
```php
return back()->withErrors(['email' => __($status)]);
return back()->with(['error' => __('auth.failed')]);
```
6. **`with()` helper key conventions:**
- Success: `'success'` key
- Error: `'error'` key
- Status: `'status'` key (Breeze default)
- Error bag: `'userDeletion'` error bag for profile deletion
7. **Exceptions thrown:**
```php
throw new \Exception("Failed to get access token from Firebase credentials."); // FcmService
throw ValidationException::withMessages([...]); // LoginRequest, ConfirmablePasswordController
```
## Validation Approach
**Three patterns detected:**
**Pattern 1 — Inline `$request->validate()`:**
```php
$validated = $request->validate([
'stationid' => 'required|string|max:20|unique:station,stationid',
'stationname' => 'required|string|max:255',
'longitude' => 'required|numeric',
'latitude' => 'required|numeric',
]);
```
- Used in `AdminController` (storeStation, updateStation, storeUser, updateUsers, updatePassword)
- Also in `AuthenticatedSessionController::store()` and `RegisteredUserController::store()`
- Rule syntax: pipe-delimited strings (`'required|string|max:255'`)
- Mixed with array syntax in some places (`['required', 'string', 'max:255']`)
**Pattern 2 — Form Request classes:**
```php
// App\Http\Requests\ProfileUpdateRequest
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255',
Rule::unique(User::class)->ignore($this->user()->id)],
];
}
```
- Used for profile updates and auth login
- `LoginRequest` includes custom `authenticate()`, `ensureIsNotRateLimited()`, `throttleKey()` methods
- LoginRequest uses array-syntax validation rules with `Rule` class
**Pattern 3 — `validateWithBag()`:**
```php
$validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
```
- Used in `PasswordController::update()`
**Validation rule patterns observed:**
- `'required'`, `'string'`, `'max:255'` most common
- `'numeric'` for coordinates
- `'confirmed'` for password confirmation
- `'email'` for email format
- `'unique:table,column'` for uniqueness
- `Rule::unique(User::class)->ignore($this->user()->id)` for unique-with-exception
- `Password::defaults()` for password strength rules
- `'current_password'` for current password verification
## Response Formatting
**Web controllers:**
```php
return view('layout.rainfall', compact('rainfallData', 'lastupdate', 'dates', 'stations', 'displayDate'));
return view('layout.admin.stationmgmt', compact('stations', 'rainfallCount', 'waterLevelCount', 'sirenCount', 'stationsCount'));
```
- Always use `view()` helper with `compact()` for variable passing
- No use of `with()` method on views — only `compact()`
**JSON responses (API):**
```php
return response()->json($graphData);
return response()->json([
'error' => false,
'id' => $user->id,
'username' => $user->name,
]);
```
- API error responses use `'error' => true/false` boolean convention
- Success response directly returns data or object
**Redirect responses:**
```php
return redirect()->back()->with('success', __('toast.stationsuccess'));
return redirect()->intended('/')->with('success', __('auth.success'). $loggedInUser->name . '.');
return Redirect::route('profile.edit')->with('status', 'profile-updated');
```
- Back with flash message
- Intended redirect for auth
- Named route redirect for profile
**PDF downloads:**
```php
$pdf = Pdf::loadView('pdf.sirenhistory', compact('sirenHistory'))
->setPaper('a4', 'potrait');
return $pdf->download('Station_Siren_History.pdf', [], ['Content-Type' => 'application/pdf']);
```
**Excel downloads:**
```php
return Excel::download(
new HourlyRainfallExport($stationid, $startDate, $endDate),
"Hourly Rainfall {$stationid} {$startDate2} - {$endDate2}.xlsx"
);
```
**Pagination:**
```php
$stations = DB::table('station')->select('station.*')->orderBy('stationid')->paginate(5);
$historyData = DB::table('station')->join(...)->paginate(10);
```
## Localization
**Pattern:**
- `__('toast.stationsuccess')` — `__()` helper for PHP
- `@lang('messages.username')` — `@lang` directive for Blade
- Language files in `lang/{en,bm}/`:
- `messages.php` — UI labels
- `toast.php` — flash messages
- `auth.php` — auth-related messages
- `validation.php` — validation error messages
- `passwords.php` — password reset strings
- `pagination.php` — pagination labels
- Locale set via `LocaleController::setLocale($lang)` with Session storage
- `LocalizationMiddleware` reads locale from session
## Commented Code Patterns
**Extensive commented-out code blocks detected:**
- `RainfallController.php` lines 209-222 — entire alternative query commented out
- `cctvController.php` lines 13-26 — entire original SQL query commented out
- `RainfallController.php` line 287 — `$station = DB::table('station')->where('stationid',$stationid)->first();`
- `RainfallController.php` line 9 — `// Add This For Export Data In Excel`
- `AppServiceProvider.php` line 23 — `// URL::forceScheme('https');`
- `FcmService.php` lines 27-30 — commented-out refresh token code
- `AlertController.php` lines 23-27 — commented-out topic mapping logic
- `web.php` lines 6-8 — commented-out route
**Comment style:**
- Single-line PHP comments `//` used throughout custom code
- PHPDoc `/** ... */` used in Breeze scaffolded controllers and model
- Custom code uses minimal PHPDoc; most methods have `// Function ...` plain comments instead
## PHPDoc Usage
**Generated/Breeze scaffold code:** Consistent PHPDoc on all methods:
```php
/**
* Display the login view.
*/
public function create(): View
```
**Custom code (RainfallController, AdminController, etc.):** Minimal to no PHPDoc:
```php
// Function Retrieve Historical Rainfall Data
public function historicalRainfall(Request $request)
```
**Models:** PHPDoc on properties using `@var list<string>` syntax:
```php
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [...];
```
## Database Access Patterns
**Dominant pattern: Raw SQL with `DB::select()` and `DB::table()`:**
- `DB::select("SELECT ...", ['param' => $value])` — raw SQL with named bindings
- `DB::table('station')->select(...)->where(...)->get()` — query builder
- `DB::raw()` for SQL fragments: `DB::raw("TO_CHAR(timestamp, 'HH24:MI') as time")`
- `collect(DB::select(...))` wrapping raw results in collections
**Eloquent used only in specific cases:**
- `User` model: `User::where('name', $request->name)->first()`, `User::factory()->create()`
- Profile CRUD: `$request->user()->fill()`, `$request->user()->save()`, `$user->delete()`
- `$user->increment('login_attempts')`, `$user->update([...])`
- No Eloquent models for `station`, `rainfall`, `waterlevel`, `siren`, `notification` tables
## Middleware Patterns
**Two custom middleware:**
- `AdminMiddleware` — checks `Auth::check()` then `Auth::user()->access_level !== 1`, redirects with error flash
- `LocalizationMiddleware` — reads `Session::get('locale')` defaults to `'en'`, calls `App::setLocale()`
- Both follow standard Laravel middleware signature: `handle(Request $request, Closure $next): Response`
## View Patterns
**Blade templates:**
- Layout inheritance: `@extends('layout.app')` with `@section('content1')`
- Includes: `@include('nav.header')`, `@include('nav.navbar')`
- Localization: `@lang('messages.username')`
- Asset: `{{ asset('logo/logomalaysia.png') }}`
- Route: `{{ route('dashboard') }}`, `{{ route('password.request') }}`
- Bootstrap 5 CSS classes used (`container`, `row`, `col-md-3`, `btn btn-primary`, `modal`, `card`, `form-select`, `pagination`)
- Alpine.js used in some views: `x-data`, `@click`, `x-show`
- Chart.js or similar used for graph rendering (inferred from JSON data endpoints)
- `@auth` / `@endauth` directive for authenticated content
## Service Layer
**One service class:**
- `FcmService` — handles Firebase Cloud Messaging push notifications
- Constructor loads credentials from `env()` config
- Methods: `sendToTopic(string $topic, string $title, string $body)`
- Uses `Google\Auth\Credentials\ServiceAccountCredentials` for auth
- Uses `Illuminate\Support\Facades\Http` for HTTP calls
## Exceptions / Custom Features
- **Export classes** implement `Maatwebsite\Excel\Concerns\FromCollection`, `WithHeadings`, `ShouldAutoSize`
- **Notification class** (`ResetPasswordNotification`) extends Laravel `Notification`, uses `Queueable`, sends via `mail`
- **No custom artisan commands** besides the default `inspire`
---
*Convention analysis: 2026-05-28*

View File

@@ -0,0 +1,193 @@
# External Integrations
**Analysis Date:** 2026-05-28
## APIs & External Services
**Firebase Cloud Messaging (FCM):**
- Service: Firebase Cloud Messaging — push notification delivery to mobile devices via topic-based messaging
- Implementation: `src/app/Services/FcmService.php`
- Auth: Service Account JSON file (path configured via `FIREBASE_CREDENTIALS` env var); uses `Google\Auth\Credentials\ServiceAccountCredentials` to fetch OAuth2 access tokens
- Scopes: `https://www.googleapis.com/auth/firebase.messaging`
- Endpoint: `https://fcm.googleapis.com/v1/projects/{projectId}/messages:send`
- Env vars: `FIREBASE_PROJECT_ID`, `FIREBASE_CREDENTIALS`
- Topic mapping: `FCM_TOPIC_RAINFALL_WARNING` env var (set in `src/app/Http/Controllers/Api/AlertController.php`)
**Google Auth Library:**
- Package: `google/auth ^1.49` (`src/composer.json`)
- Purpose: Generates OAuth2 bearer tokens for Firebase API calls using service account credentials
- Implementation: `src/app/Services/FcmService.php` — creates `ServiceAccountCredentials` instance and calls `fetchAuthToken()`
**Amazon SES (Simple Email Service):**
- Configured in `src/config/services.php``ses` key with `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`
- Mail transport: `ses` driver in `src/config/mail.php`
- Also used as a transport in `roundrobin` failover group
**Postmark:**
- Configured in `src/config/services.php``postmark` key with `POSTMARK_API_KEY` env var
- Mail transport: `postmark` driver in `src/config/mail.php`
- Optional `message_stream_id` config commented out
- Used in `roundrobin` failover group with SES
**Resend:**
- Configured in `src/config/services.php``resend` key with `RESEND_API_KEY` env var
- Mail transport: `resend` driver in `src/config/mail.php`
**Slack:**
- Notifications: configured in `src/config/services.php``slack.notifications` with `SLACK_BOT_USER_OAUTH_TOKEN` and `SLACK_BOT_USER_DEFAULT_CHANNEL` env vars
- Logging: Slack webhook channel available in `src/config/logging.php``LOG_SLACK_WEBHOOK_URL` env var for critical-level log alerts
**Papertrail:**
- Logging channel configured in `src/config/logging.php``SyslogUdpHandler` via `PAPERTRAIL_URL` and `PAPERTRAIL_PORT` env vars with TLS
## Data Storage
**Databases:**
- PostgreSQL 18.1
- Container: `sides-db` (image `postgres:18.1`)
- Connection: `pgsql` driver; env vars `DB_HOST`, `DB_PORT` (5432), `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`
- Persistence: `./docker/postgres/data:/var/lib/postgres/data`
- Exposed port: 5432
- Schema: `public`
- Client library: `pdo_pgsql` and `pgsql` PHP extensions installed in Docker image
**Redis:**
- Configured in `src/config/database.php` under the `redis` key
- Client: `phpredis` (configured via `REDIS_CLIENT` env var)
- Separate connections for `default` (DB 0) and `cache` (DB 1)
- Retry/backoff: decorrelated jitter strategy, max 3 retries, 100ms base, 1000ms cap
- Used as optional driver for: cache (`CACHE_STORE=redis`), queue (`QUEUE_CONNECTION=redis`), session (`SESSION_DRIVER=redis`)
**File Storage:**
- Local disk (`local`): `storage/app/private/` — default filesystem
- Public disk (`public`): `storage/app/public/` — symlinked to `public/storage`
- S3 disk (`s3`) configured for AWS S3: env vars `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`, `AWS_BUCKET`, `AWS_URL`, `AWS_ENDPOINT`
- Default disk: selected via `FILESYSTEM_DISK` env var (falls back to `local`)
## Authentication & Identity
**Auth Provider:**
- Laravel's built-in session-based authentication (`src/config/auth.php`)
- Guard: `web` driver using `session`
- Provider: `users` using `eloquent` driver on `App\Models\User`
- Password resets via `password_reset_tokens` table; 60-minute expiry; 60-second throttle
- Custom `access_level` field on `users` table (integer, default `2`), with `is_blocked` and `login_attempts` fields for account lockout
- API authentication: custom tokenless login in `src/app/Http/Controllers/Api/AuthController.php` — validates username/password via raw SQL and `Hash::check()`, returns user data with `access_level`
**Email Verification:**
- Built-in Laravel email verification flow (`MustVerifyEmail` trait available but not used in `App\Models\User`)
- Routes use `signed` middleware with throttling (`6:1`)
## Monitoring & Observability
**Error Tracking:**
- Not detected — no Sentry, Bugsnag, or similar APM integration
**Logs:**
- Laravel logging via Monolog (`src/config/logging.php`)
- Default channel: `stack``single` (writes to `storage/logs/laravel.log`)
- Available channels: `single`, `daily` (14-day retention), `slack`, `papertrail`, `stderr`, `syslog`, `errorlog`
- `pail` CLI log viewer available in dev (`laravel/pail ^1.2.2`)
**Dozzle:**
- Log viewer container (`amir20/dozzle:latest`) in `docker-compose.yml`
- Port: 777
- Container actions and shell access enabled
## CI/CD & Deployment
**Hosting:**
- Not explicitly declared — Docker-based deployment assumed
- Artisan serve on port 8000 (dev); Nginx on port 8080/8443 (production via Docker)
- `composer setup` script performs full bootstrap: `composer install`, `.env` creation, `key:generate`, migrations, npm install + build
**CI Pipeline:**
- Not detected — no GitHub Actions, GitLab CI, or similar config files found
## Environment Configuration
**Required env vars (from code analysis):**
| Variable | Used In | Purpose |
|---|---|---|
| `APP_KEY` | `config/app.php` | Laravel encryption key (AES-256-CBC) |
| `APP_ENV` | `config/app.php` | Environment (local/production) |
| `APP_DEBUG` | `config/app.php` | Debug mode toggle |
| `APP_URL` | `config/app.php`, `config/filesystems.php` | Application base URL |
| `APP_NAME` | `config/app.php`, `config/session.php`, `config/cache.php` | Application name |
| `DB_CONNECTION` | `config/database.php`, `config/queue.php` | Database connection (default: `sqlite`) |
| `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD` | `config/database.php` | PostgreSQL connection |
| `SESSION_DRIVER` | `config/session.php` | Session backend (default: `database`) |
| `SESSION_DRIVER` | `config/session.php` | Session backend (default: `database`) |
| `QUEUE_CONNECTION` | `config/queue.php` | Queue driver (default: `database`) |
| `CACHE_STORE` | `config/cache.php` | Cache backend (default: `database`) |
| `FILESYSTEM_DISK` | `config/filesystems.php` | Storage disk (default: `local`) |
| `MAIL_MAILER` | `config/mail.php` | Mail driver (default: `log`) |
| `FIREBASE_PROJECT_ID` | `FcmService.php` | Firebase project identifier |
| `FIREBASE_CREDENTIALS` | `FcmService.php` | Path to Firebase service account JSON |
| `FCM_TOPIC_RAINFALL_WARNING` | `AlertController.php` | FCM topic for alerts |
| `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` | `docker-compose.yml` | PostgreSQL container credentials |
| `PGADMIN_EMAIL`, `PGADMIN_PASSWORD` | `docker-compose.yml` | pgAdmin login credentials |
**Secrets location:**
- Environment variables via `.env` file (not committed per `.env.example` pattern)
- Docker Compose `environment` blocks reference `${VAR}` from `.env` file (no hardcoded secrets)
## Frontend CDN Dependencies
**Loaded via CDN (not bundled by Vite):**
- Bootstrap 5.3.3 (CSS + JS bundle) — `cdn.jsdelivr.net`
- Leaflet 1.9.4 (CSS + JS) — `unpkg.com`
- Chart.js 4.4.0 + chartjs-plugin-zoom 2.1.1 — `cdn.jsdelivr.net`
- jsPDF 2.5.1 + jspdf-autotable 3.5.28 — `cdnjs.cloudflare.com`
- jQuery 3.6.0 — `cdn.jsdelivr.net`
- Boxicons 2.1.4 — `unpkg.com`
- Flatpickr — `cdn.jsdelivr.net`
## Webhooks & Callbacks
**Incoming:**
- Not detected — no webhook endpoint routes found
**Outgoing:**
- Firebase FCM HTTP POST to `fcm.googleapis.com` for push notifications (`FcmService.php`)
- Slack webhook for log alerts (configured via `LOG_SLACK_WEBHOOK_URL`)
## Queue & Job Processing
**Queue Driver:**
- Default: `database` (using PostgreSQL `jobs` table)
- Alternate drivers configured: `sync`, `beanstalkd`, `sqs`, `redis`, `deferred`, `background`, `failover`
- Failover chain: `database``deferred`
- Job batching table: `job_batches`
- Failed jobs table: `failed_jobs` (driver: `database-uuids`)
**Queue Worker:**
- Dev command: `php artisan queue:listen --tries=1` (part of `composer dev` script, runs via `concurrently`)
## Session Management
**Session Driver:**
- Default: `database` (using PostgreSQL `sessions` table)
- Alternate drivers available: `file`, `cookie`, `memcached`, `redis`, `dynamodb`, `array`
- Lifetime: 120 minutes (configurable via `SESSION_LIFETIME`)
- Cookie: `{app_name}-session`, HTTP-only, SameSite=Lax
## Localization
**Supported Locales:**
- English (`en`): `src/lang/en/``messages.php`, `toast.php`, `auth.php`, `validation.php`, `pagination.php`, `passwords.php`
- Bahasa Malaysia (`bm`): `src/lang/bm/` — same file set as English
- Locale switching: `src/app/Http/Controllers/LocaleController.php` via `GET /locale/{locale}` route
- Stored in session via `Session::put('locale', $lang)`
## Management & Utility Containers
**Docker auxiliary services (in `docker-compose.yml`):**
- **pgAdmin4** (`dpage/pgadmin4`) - Port 5050, for PostgreSQL management
- **Adminer** - Port 6060, lightweight DB management
- **Dozzle** (`amir20/dozzle:latest`) - Port 777, real-time Docker log viewer
- **Filebrowser** (`gtstef/filebrowser:stable`) - Port 8900, web file manager with access to `/root/sides`
---
*Integration audit: 2026-05-28*

116
.planning/codebase/STACK.md Normal file
View File

@@ -0,0 +1,116 @@
# Technology Stack
**Analysis Date:** 2026-05-28
## Languages
**Primary:**
- PHP 8.2 - Server-side application logic; runtime via `php:8.2-fpm` base image in `Dockerfile`
**Secondary:**
- JavaScript (ES Modules) - Frontend interactivity (Alpine.js, Chart.js, Leaflet maps)
- CSS / PostCSS - Styling with Tailwind CSS utility classes
- Blade templates - Server-side templating language (`.blade.php` files)
- SQL - Raw PostgreSQL queries used extensively in controllers
## Runtime
**Environment:**
- Docker containers orchestrated by Docker Compose (version `3.9`)
- PHP-FPM 8.2 (`php:8.2-fpm` image) for PHP processing
- Nginx `stable-alpine` as reverse proxy / static file server
**Package Manager:**
- Composer 2.x (copied from `composer:2.3` image) for PHP dependencies
- npm (Node.js LTS installed in Docker image) for frontend dependencies
- Lockfile: `composer.lock` and `package-lock.json` (both present)
## Frameworks
**Core:**
- Laravel 12.0 (`laravel/framework ^12.0`) - Full-stack PHP web framework
- Eloquent ORM (minimal usage — predominantly raw DB queries)
- Blade templating engine
- Artisan CLI for management commands
- Built-in auth scaffolding via `laravel/breeze ^2.3` (dev dependency)
**Testing:**
- PHPUnit 11.5.3 (`phpunit/phpunit ^11.5.3`) - Unit and feature testing
- Mockery 1.6 (`mockery/mockery ^1.6`) - Mocking framework
- Laravel Pail 1.2.2 (`laravel/pail ^1.2.2`) - Log viewer for CLI (dev)
- Collision 8.6 (`nunomaduro/collision ^8.6`) - Error reporting (dev)
**Build/Dev:**
- Vite 7.0.7 (`vite ^7.0.7`) - Frontend build tool / bundler
- Laravel Vite Plugin 2.0 (`laravel-vite-plugin ^2.0`) - Vite integration for Laravel
- PostCSS 8.4.31 (`postcss ^8.4.31`) - CSS processing pipeline
- Autoprefixer 10.4.2 (`autoprefixer ^10.4.2`) - Vendor prefix injection
- Laravel Pint 1.24 (`laravel/pint ^1.24`) - PHP code style fixer (dev)
- Laravel Sail 1.41 (`laravel/sail ^1.41`) - Docker dev environment (dev)
## Key Dependencies
**Critical:**
- `google/auth ^1.49` - Google OAuth2 authentication library; used for Firebase Cloud Messaging (FCM) service account token generation
- `barryvdh/laravel-dompdf ^3.1` - PDF generation from Blade views (used for siren history, rainfall history, water level history exports)
- `maatwebsite/excel ^3.1` - Excel export (XLSX) for historical rainfall and water level data
- `laravel/tinker ^2.10.1` - Interactive REPL for Laravel
**Infrastructure:**
- PostgreSQL 18.1 (via `postgres:18.1` Docker image) - Primary database
- Nginx `stable-alpine` - Web server / reverse proxy
- Redis (configured in `config/database.php` via `phpredis` client) - Cache/queue driver option
- pgAdmin4 (`dpage/pgadmin4`) - Database administration UI
- Adminer - Lightweight database management UI
**Frontend:**
- Tailwind CSS 3.1 (`tailwindcss ^3.1.0`) - Utility-first CSS framework
- `@tailwindcss/forms ^0.5.2` - Form reset plugin
- `@tailwindcss/vite ^4.0.0` - Tailwind+Vite integration
- Alpine.js 3.4.2 (`alpinejs ^3.4.2`) - Lightweight JS framework for interactivity
- Axios 1.11.0 (`axios ^1.11.0`) - HTTP client for API requests
- Bootstrap 5.3.3 - Loaded via CDN for UI components (modals, toasts, dropdowns)
- Chart.js 4.4.0 - Loaded via CDN; chart rendering with zoom plugin 2.1.1
- Leaflet 1.9.4 - Loaded via CDN; interactive map for station visualization
- jQuery 3.6.0 - Loaded via CDN for DOM manipulation
- Flatpickr - Loaded via CDN; date picker widget
- Boxicons 2.1.4 - Loaded via CDN; icon library
- jsPDF 2.5.1 + jspdf-autotable 3.5.28 - Loaded via CDN; client-side PDF generation
## Configuration
**Environment:**
- `.env` file (`.env.example` pattern for initial setup via `composer setup` script)
- Config files in `src/config/` — all values read from `env()` with sensible defaults
**Build:**
- `src/vite.config.js` — Vite entry points: `resources/css/app.css`, `resources/js/app.js`, `resources/css/style.css`, `resources/js/script.js`
- `src/tailwind.config.js` — Content paths for PurgeCSS; custom font family (Figtree); forms plugin
- `src/postcss.config.js` — PostCSS pipeline (default Vite setup)
## Platform Requirements
**Development:**
- Docker Engine + Docker Compose (preferred via `docker-compose.yml`)
- PHP 8.2+ CLI (if running outside Docker)
- Composer 2.x
- Node.js LTS + npm
**Production:**
- Docker container orchestration (Docker Compose or similar)
- PostgreSQL 18.1 database
- Redis (optional, for queue/cache/session in production config)
- Nginx with PHP-FPM
## Database
**Primary:**
- PostgreSQL 18.1 (via Docker image `postgres:18.1` in `docker-compose.yml`)
- Port: 5432 (exposed)
- Connection configured in Laravel via `config/database.php` (`pgsql` driver)
- Default schema: `public`
- All migrations use raw PostgreSQL-specific SQL (e.g., `::date` casts, `INTERVAL`, `date_trunc`)
---
*Stack analysis: 2026-05-28*

View File

@@ -0,0 +1,360 @@
# Codebase Structure
**Analysis Date:** 2026-05-28
## Directory Layout
```
src/ # Laravel application root
├── app/ # Application code
│ ├── Exports/ # Excel export classes (Maatwebsite)
│ ├── Http/ # HTTP layer
│ │ ├── Controllers/ # Web controllers
│ │ │ ├── Api/ # API controllers (JSON)
│ │ │ └── Auth/ # Auth controllers (Laravel Breeze)
│ │ ├── Middleware/ # Custom middleware
│ │ └── Requests/ # Form request validation classes
│ │ └── Auth/ # Auth-specific form requests
│ ├── Models/ # Eloquent models
│ ├── Notifications/ # Notification classes
│ ├── Providers/ # Service providers
│ ├── Services/ # Service classes
│ └── View/ # Blade component classes
│ └── Components/ # View components
├── bootstrap/ # Framework bootstrap
│ ├── app.php # App config (middleware, routing registration)
│ └── providers.php # Registered service providers
├── config/ # Configuration files
├── database/ # Database layer
│ ├── factories/ # Model factories
│ ├── migrations/ # Schema migrations (12 files)
│ └── seeders/ # Database seeders
├── lang/ # Localization files
│ ├── bm/ # Bahasa Malaysia
│ └── en/ # English
├── public/ # Public web root
│ ├── build/ # Vite build output
│ ├── css/ # Compiled CSS
│ ├── icon/ # Map icons
│ ├── img/ # Images
│ ├── js/ # Custom JS
│ └── logo/ # Logo files
├── resources/ # Frontend resources
│ ├── css/ # Source CSS (Tailwind + custom)
│ ├── js/ # Source JS
│ └── views/ # Blade templates
│ ├── auth/ # Auth screens (6 files)
│ ├── components/ # Shared Blade components (13 files)
│ ├── layout/ # Main app views
│ │ ├── admin/ # Admin panel views
│ │ ├── graph/ # Chart views
│ │ ├── notification/ # Notification views
│ │ │ └── history/ # Notification history views
│ │ └── siren/ # Siren views
│ ├── layouts/ # Base layout templates (Breeze)
│ ├── nav/ # Navigation partials (header, navbar, footer)
│ ├── pdf/ # PDF export templates
│ └── profile/ # Profile views
│ └── partials/
├── routes/ # Route definitions
├── storage/ # Framework storage
├── tests/ # Automated tests
│ ├── Feature/ # Feature tests
│ │ └── Auth/ # Auth feature tests
│ ├── Unit/ # Unit tests
│ └── storage/ # Test storage
├── vendor/ # Composer dependencies (excluded)
├── composer.json # PHP dependencies
├── package.json # Node dependencies (Vite, Tailwind)
├── vite.config.js # Vite configuration
├── tailwind.config.js # Tailwind CSS config
└── phpunit.xml # PHPUnit config
```
## Directory Purposes
**`app/Http/Controllers/` — Web Controllers:**
- Purpose: Handle authenticated web requests, query data, render Blade views
- Contains: 11 controllers + 9 auth controllers + 3 API controllers
- Key files:
- `MapController.php`: Dashboard page — joins station + latest rainfall/waterlevel/siren data
- `RainfallController.php`: Rainfall data display, 6-hour threshold, historical data, graph data, Excel export
- `WaterLevelController.php`: Water level display, graph data, historical data, Excel export
- `SirenController.php`: Siren status display, siren history, PDF export
- `NotificationController.php`: Rainfall/waterlevel/siren notifications, history, PDF exports
- `AdminController.php`: Station CRUD, user CRUD (access_level=1 only)
- `cctvController.php`: CCTV link display for stations
- `LocaleController.php`: Language switching (en/bm)
- `ProfileController.php`: User profile edit/update/delete
- `Controller.php`: Empty abstract base class
**`app/Http/Controllers/Api/` — API Controllers:**
- Purpose: JSON endpoints for external IoT devices and mobile clients
- Contains: 3 controllers (no middleware/auth required on most endpoints)
- Key files:
- `StationController.php`: 7 endpoints — current data, rainfall, waterlevel, notifications, history, siren, siren history
- `AuthController.php`: Custom username/password login returning JSON
- `AlertController.php`: Relay station alerts via FCM push
**`app/Http/Controllers/Auth/` — Auth Controllers (Laravel Breeze):**
- Purpose: Handle authentication flow (login, register, password reset, email verification)
- Contains: 9 controllers scaffolded by Laravel Breeze
- Key files:
- `AuthenticatedSessionController.php`: Login with custom blocked-user + login-attempt tracking
- `RegisteredUserController.php`: User registration
- `NewPasswordController.php`, `PasswordController.php`, `PasswordResetLinkController.php`: Password management
- `VerifyEmailController.php`, `EmailVerificationNotificationController.php`, `EmailVerificationPromptController.php`: Email verification
- `ConfirmablePasswordController.php`: Password confirmation
**`app/Http/Middleware/` — Custom Middleware:**
- Purpose: Request filtering and modification
- Contains: 2 middleware classes
- Key files:
- `AdminMiddleware.php`: Checks `Auth::check()` + `Auth::user()->access_level === 1`, redirects to `/dashboard` on failure. Registered as alias `'admin'` in `bootstrap/app.php`.
- `LocalizationMiddleware.php`: Reads `locale` from session, sets `App::setLocale()`. Appended to `web` middleware group in `bootstrap/app.php`.
**`app/Http/Requests/` — Form Request Validation:**
- Purpose: Encapsulate form validation and authorization logic
- Contains: 2 form request classes
- Key files:
- `ProfileUpdateRequest.php`: Validates name + email (unique check ignoring current user)
- `Auth/LoginRequest.php`: Rate-limited login validation (5 attempts), custom `username()` returning `'name'`
**`app/Models/` — Eloquent Models:**
- Purpose: Single model for Laravel authentication
- Contains: `User.php` — only Eloquent model in the app
- Key details:
- Extends `Authenticatable`, uses `HasFactory`, `Notifiable`
- Fillable: `name`, `email`, `password`, `access_level`, `login_attempts`, `is_blocked`
- Hidden: `password`, `remember_token`
- Casts: `email_verified_at` (datetime), `password` (hashed), `is_blocked` (boolean)
- Overrides `sendPasswordResetNotification()` to use custom `ResetPasswordNotification`
**`app/Services/` — Service Layer:**
- Purpose: External API integrations
- Contains: 1 service class
- Key file:
- `FcmService.php`: Firebase Cloud Messaging integration. Constructor reads `FIREBASE_PROJECT_ID` and `FIREBASE_CREDENTIALS` (path to JSON) from env. `sendToTopic()` authenticates via `google/auth` `ServiceAccountCredentials`, sends POST to FCM v1 HTTP API.
**`app/Exports/` — Excel Exports:**
- Purpose: Generate downloadable Excel spreadsheets
- Contains: 2 export classes using `Maatwebsite\Excel`
- Key files:
- `HourlyRainfallExport.php`: Exports hourly rainfall with 24-column pivot (hour_00..hour_23) for a date range. Implements `FromCollection`, `WithHeadings`, `ShouldAutoSize`.
- `WaterLevelExport.php`: Exports water level readings with alert/warning/danger thresholds for a station+date. Implements `FromCollection`, `WithHeadings`, `ShouldAutoSize`.
**`app/Notifications/` — Notification Classes:**
- Purpose: Email notifications sent by Laravel
- Contains: 1 notification class
- Key file:
- `ResetPasswordNotification.php`: Queueable email notification for password reset. Sends via `mail` channel with SIDES branding.
**`app/Providers/` — Service Providers:**
- Purpose: Framework bootstrapping
- Contains: 1 service provider
- Key file:
- `AppServiceProvider.php`: Currently empty `register()` and `boot()` methods (no custom services registered).
**`app/View/Components/` — Blade Components:**
- Purpose: Render Blade component views
- Contains: 2 component classes
- Key files:
- `AppLayout.php`: Renders `layouts.app` (Breeze default authenticated layout)
- `GuestLayout.php`: Renders `layouts.guest` (Breeze default guest layout)
**`routes/` — Route Definitions:**
- Purpose: Map HTTP requests to controllers
- Contains: 4 route files
- Key files:
- `web.php` (85 lines): All web routes — auth-protected (dashboard, rainfall, waterlevel, siren, notification, CCTV), admin-protected (station management, user management), and public (dashboard, stations, locale switch). Includes `auth.php` at the end.
- `auth.php` (59 lines): Auth routes scaffolded by Breeze — guest routes (login, register, password reset), auth routes (verify email, confirm password, logout).
- `api.php` (19 lines): Public API routes — station data endpoints (7 GET), login, alert push. No middleware required on most endpoints.
- `console.php` (8 lines): Single `inspire` Artisan command (default Laravel).
**`database/migrations/` — Schema Migrations:**
- Purpose: Define and evolve database schema
- Contains: 12 migration files ordered by timestamp
- Key files:
- `0001_01_01_000000_create_users_table.php`: Users, password_reset_tokens, sessions tables (Laravel default)
- `2025_11_06_071853_create_station_table.php`: `station` table — stationid (PK), name, district, lng, lat, mainriverbasin, subriverbasin, rainfall, waterlevel
- `2025_11_06_072709_create_rainfall__table.php`: `rainfall` table — stationid, timestamp, anncum, daily, hourly, currentrf, battery
- `2025_11_06_072738_create_waterlevel_table.php`: `waterlevel` table — stationid, datetime, waterlevel, alert, warning, danger
- `2025_11_07_024940_create_notification_table.php`: `notification` table — stationid, timestamp, stationtype, level, active_time
- `2025_11_07_031601_create_siren__table.php`: `siren` table — stationid, stationtype, active_time, level
- `2025_11_07_063825_add_access_level_to_users_table.php`: Adds `access_level` (int, default 2) to users
- `2025_11_07_065418_add_block_fields_to_users_table.php`: Adds `is_blocked` (boolean) and `login_attempts` (int) to users
- `2025_11_08_004548_add_siren_to_station_table.php`: Adds `siren` (int, nullable) to station
- `2025_11_25_113158_add_cctvlink_to_station.php`: Adds `cctv_link` (string, nullable) to station
**`resources/views/` — Blade Templates:**
- Purpose: Render all HTML and PDF output
- Contains: ~40+ Blade template files organized by domain
- Structure:
- `layout/app.blade.php` — Main authenticated layout (`@include nav.header+navbar`, `@yield content1/content2/content3`)
- `layout/homeapp.blade.php` — Public layout (no auth, full-width)
- `layout/dashboard.blade.php` — Home page with Leaflet map + station data table
- `layout/rainfall.blade.php` — Rainfall data table with 7-day daily columns + graph modal
- `layout/waterlevel.blade.php` — Water level data table
- `layout/threshold.blade.php` — 6-hour early warning threshold view
- `layout/historicalrainfall.blade.php` — Historical rainfall with date range picker
- `layout/historicalwl.blade.php` — Historical water level
- `layout/cctv.blade.php` — CCTV links for stations
- `layout/home.blade.php` — Public landing page (extends `layout.homeapp`)
- `layout/siren/home.blade.php`, `layout/siren/history.blade.php` — Siren status and history
- `layout/notification/rainfall.blade.php`, `layout/notification/waterlevel.blade.php`, `layout/notification/siren.blade.php` — Current notifications
- `layout/notification/history/rainfall.blade.php`, `layout/notification/history/waterlevel.blade.php` — Notification history
- `layout/graph/rainfall.blade.php` — Rainfall graph page
- `layout/admin/stationmgmt.blade.php` (352 lines) — Station management CRUD interface
- `layout/admin/usermgmt.blade.php` — User management CRUD interface
- `pdf/rfhistory.blade.php`, `pdf/wlhistory.blade.php`, `pdf/sirenhistory.blade.php` — PDF export templates
- `nav/header.blade.php` — HTML head, CSS, Leaflet, Flatpickr, jQuery CDNs
- `nav/navbar.blade.php` — Full responsive navigation (desktop + mobile sidebar)
- `nav/footer.blade.php` — Footer partial
- `auth/*.blade.php` — Login, register, forgot/reset password, verify-email, confirm-password screens
- `components/*.blade.php` — 13 shared Blade components (application-logo, dropdown, input-label, modal, nav-link, buttons, etc.)
- `layouts/app.blade.php`, `layouts/guest.blade.php` — Breeze default layouts (component-based)
- `profile/edit.blade.php` — Profile edit page
**`lang/` — Localization Files:**
- Purpose: Internationalization strings for two locales
- Contains: `en/` (English) and `bm/` (Bahasa Malaysia) directories
- Key files: `messages.php` (UI text), `toast.php` (flash messages), `auth.php`, `validation.php`, `passwords.php`, `pagination.php`
**`public/` — Public Web Root:**
- Purpose: Entry point for HTTP requests (`index.php`), static assets
- Contains: Compiles CSS (`css/style.css`), JavaScript (`js/script.js`, `js/graph.js`), Leaflet map icons (`icon/`), images (`img/`), logos (`logo/`), Vite build output (`build/`)
**`tests/` — Automated Tests:**
- Purpose: PHPUnit test suite
- Contains: Feature tests and unit tests
- Key files:
- `tests/Feature/Auth/`: 6 auth test files (most appear empty — e.g., `AuthenticationTest.php` has 0 lines)
- `tests/Feature/ExampleTest.php`, `tests/Feature/ProfileTest.php`
- `tests/Unit/ExampleTest.php`
## Key File Locations
**Entry Points:**
- `public/index.php`: HTTP entry point (Laravel front controller)
- `artisan`: CLI entry point
- `bootstrap/app.php`: Application configuration (middleware aliases, routing, exceptions)
- `bootstrap/providers.php`: Registered service providers
**Configuration:**
- `config/app.php`: App name, env, URL, timezone, locale, providers, aliases
- `config/database.php`: DB connection (default: sqlite, but code uses PostgreSQL)
- `config/auth.php`: Auth guard (`web`, session), user provider, password reset config
- `config/mail.php`: Mail driver config (default: log)
- `config/filesystems.php`: Disk configs (local, public, s3)
- `config/services.php`: Third-party API keys (postmark, resend, ses, slack)
- `config/queue.php`: Queue driver (default: database)
- `config/session.php`: Session driver config
- `config/cache.php`: Cache driver config
- `.env`: Environment variables (exists but not read — contains credentials)
**Core Routing:**
- `routes/web.php`: All web routes (85 lines)
- `routes/auth.php`: Auth routes (59 lines, included from web.php)
- `routes/api.php`: API routes (19 lines)
- `routes/console.php`: Console commands (8 lines)
**Database Schema:**
- `database/migrations/`: 12 migration files defining 8+ tables
- `database/seeders/DatabaseSeeder.php`: Seeds admin user
**Static Assets:**
- `resources/css/app.css`: Tailwind CSS source
- `resources/css/style.css`: Custom CSS
- `resources/js/app.js`: Alpine.js + Bootstrap app JS
- `resources/js/bootstrap.js`: Axios + Echo bootstrap
- `resources/js/script.js`: Custom app scripts
- `resources/js/graph.js`: Chart.js graph rendering
## Naming Conventions
**Files:**
- **Controllers**: PascalCase suffixed with `Controller` (e.g., `RainfallController.php`, `AdminController.php`)
- **Models**: PascalCase singular (only `User.php`)
- **Middleware**: PascalCase suffixed with `Middleware` (e.g., `AdminMiddleware.php`)
- **Requests**: PascalCase suffixed with `Request` (e.g., `ProfileUpdateRequest.php`)
- **Exports**: PascalCase suffixed with `Export` (e.g., `HourlyRainfallExport.php`)
- **Services**: PascalCase suffixed with `Service` (e.g., `FcmService.php`)
- **Notifications**: PascalCase suffixed with `Notification` (e.g., `ResetPasswordNotification.php`)
- **Migrations**: `YYYY_MM_DD_HHMMSS_create_{table}_table.php` or `..._add_{column}_to_{table}_table.php`
- **Blade views**: Lowercase kebab-case (e.g., `historicalrainfall.blade.php`, `stationmgmt.blade.php`)
- **Routes file**: Lowercase (`web.php`, `api.php`, `auth.php`, `console.php`)
- **Config files**: Lowercase (`app.php`, `database.php`)
**Directories:**
- Namespace-matching PascalCase under `app/` (e.g., `Http/Controllers/Api/`)
- Lowercase under `resources/views/` (e.g., `layout/admin/`, `layout/notification/history/`)
- Lowercase under `config/`, `database/`, `routes/`, `lang/`
## Where to Add New Code
**New Feature (e.g., new data type):**
- Controller: `app/Http/Controllers/{Name}Controller.php` — extends `Controller`, uses `DB` facade
- Routes: `routes/web.php` — add under appropriate middleware group (`auth` or `admin`)
- Views: `resources/views/layout/{name}/` — create view directory with `home.blade.php`, `history.blade.php` etc.
- API endpoint: `routes/api.php` + `app/Http/Controllers/Api/{Name}Controller.php`
- Exports: `app/Exports/{Name}Export.php` + `resources/views/pdf/{name}history.blade.php` if needed
**New API Endpoint:**
- Add route to `routes/api.php`
- Add method to existing controller in `app/Http/Controllers/Api/` or create new controller
**New Middleware:**
- Create class in `app/Http/Middleware/`
- Register alias or group in `bootstrap/app.php``withMiddleware()`
**New Service:**
- Create class in `app/Services/`
- Inject via constructor or `app()` helper in controllers
**New Database Table:**
- Create migration in `database/migrations/` using `YYYY_MM_DD_HHMMSS_create_{table}_table.php` format
- Add seeder entry to `database/seeders/DatabaseSeeder.php`
**New Translation:**
- Add key to both `lang/en/messages.php` and `lang/bm/messages.php` (or corresponding domain file)
- Use `@lang('messages.key')` in Blade or `__('messages.key')` in PHP
**New Validation:**
- Add inline `$request->validate([...])` for simple cases
- Create `app/Http/Requests/{Name}Request.php` for reusable validation
## Special Directories
**`resources/views/layout/`:**
- Purpose: Main application views (the primary view directory used by all web controllers)
- Contains: 14 files + 4 subdirectories (admin/, graph/, notification/, siren/)
- The `layout.app` Blade layout is the base template accessed via `@extends('layout.app')` — this includes nav, header, footer, modals, scripts, CSS
**`resources/views/layouts/`:**
- Purpose: Laravel Breeze default layout templates (used by auth screens and view components)
- Contains: `app.blade.php` (component-based slot layout), `guest.blade.php`, `navigation.blade.php`
- This is a SEPARATE layout system from `resources/views/layout/` — the application uses `layout.app` for authenticated pages and `layouts.app` only for auth scaffolding views
**`public/js/` and `public/css/`:**
- Purpose: Compiled/minified static assets (not source)
- Contains: `script.js`, `graph.js` — JavaScript files directly placed in public (NOT compiled through Vite). These are loaded via `<script src="{{ asset('js/script.js') }}">` in the `layout.app` Blade layout.
- Source JS is in `resources/js/` and compiled via Vite.
**`storage/`:**
- Purpose: Framework runtime storage (logs, cache, sessions, views)
- Generated: Yes
- Committed: No
**`vendor/`:**
- Purpose: Composer PHP dependencies
- Generated: Yes
- Committed: No
**`node_modules/`:**
- Purpose: NPM frontend dependencies
- Generated: Yes
- Committed: No
---
*Structure analysis: 2026-05-28*

View File

@@ -0,0 +1,318 @@
# Testing Patterns
**Analysis Date:** 2026-05-28
## Test Framework
**Runner:**
- PHPUnit `^11.5.3` (installed via Composer)
- Config: `src/phpunit.xml`
**Assertion Library:**
- PHPUnit native assertions (`$this->assertTrue()`, `$this->assertSame()`, `$this->assertNull()`, `$this->assertGuest()`)
- Laravel test response assertions (`$response->assertStatus()`, `$response->assertOk()`, `$response->assertSessionHasNoErrors()`, `$response->assertRedirect()`, `$response->assertSessionHasErrorsIn()`)
**Mocking:**
- Mockery `^1.6` available as dependency
- No mock usage detected in existing tests
**Run Commands:**
```bash
composer test # Runs: php artisan config:clear --ansi && php artisan test
./vendor/bin/phpunit # Direct PHPUnit invocation
```
## phpunit.xml Configuration
**File:** `src/phpunit.xml`
**Key settings:**
- `colors="true"` — colored output
- `bootstrap="vendor/autoload.php"`
- Test suites: `Unit` (`tests/Unit`) and `Feature` (`tests/Feature`)
- Source inclusion: `app` directory
- Environment overrides:
- `APP_ENV=testing`
- `DB_CONNECTION=sqlite`
- `DB_DATABASE=:memory:` — in-memory SQLite for tests
- `CACHE_STORE=array`
- `SESSION_DRIVER=array`
- `MAIL_MAILER=array`
- `QUEUE_CONNECTION=sync`
- `BCRYPT_ROUNDS=4` — faster password hashing
- `BROADCAST_CONNECTION=null`
- `PULSE_ENABLED=false`, `TELESCOPE_ENABLED=false`, `NIGHTWATCH_ENABLED=false`
## Test File Organization
**Location:**
- Feature tests: `src/tests/Feature/`
- Unit tests: `src/tests/Unit/`
**Naming:**
- PascalCase class names matching the tested resource: `ExampleTest`, `ProfileTest`, `AuthenticationTest`
- Method names: snake_case descriptive names: `test_the_application_returns_a_successful_response`, `test_profile_page_is_displayed`
- Method prefix: `test_` prefix convention
**Directory Structure:**
```
tests/
├── Feature/
│ ├── Auth/
│ │ ├── AuthenticationTest.php (empty)
│ │ ├── EmailVerificationTest.php (empty)
│ │ ├── PasswordConfirmationTest.php (empty)
│ │ ├── PasswordResetTest.php (empty)
│ │ ├── PasswordUpdateTest.php (empty)
│ │ └── RegistrationTest.php (empty)
│ ├── ExampleTest.php
│ └── ProfileTest.php
├── Unit/
│ └── ExampleTest.php
└── TestCase.php
```
**Base class:** `src/tests/TestCase.php`
```php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}
```
- Extends `Illuminate\Foundation\Testing\TestCase`
- Empty — no custom setup or helper methods defined
- All Feature tests extend `Tests\TestCase`
## Test Structure
**Suite Organization:**
Feature tests follow standard Laravel testing patterns:
```php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ProfileTest extends TestCase
{
use RefreshDatabase;
public function test_profile_page_is_displayed(): void
{
$user = User::factory()->create();
$response = $this
->actingAs($user)
->get('/profile');
$response->assertOk();
}
}
```
Unit tests use plain PHPUnit:
```php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}
```
**Patterns:**
- **Arrange-Act-Assert** structure clearly visible
- Variable names: `$user`, `$response`
- Method return type `: void` consistently used
- Chained HTTP methods: `$this->actingAs($user)->patch('/profile', [...])->assertSessionHasNoErrors()`
## Database Testing
**Approach:**
- In-memory SQLite (`DB_DATABASE=:memory:`) for all tests
- `RefreshDatabase` trait used in `ProfileTest` to migrate schema before each test
- `User::factory()->create()` for creating test users
**ProfileTest database patterns:**
```php
public function test_profile_information_can_be_updated(): void
{
$user = User::factory()->create();
$response = $this
->actingAs($user)
->patch('/profile', [
'name' => 'Test User',
'email' => 'test@example.com',
]);
$response
->assertSessionHasNoErrors()
->assertRedirect('/profile');
$user->refresh();
$this->assertSame('Test User', $user->name);
$this->assertSame('test@example.com', $user->email);
$this->assertNull($user->email_verified_at);
}
public function test_user_can_delete_their_account(): void
{
$user = User::factory()->create();
$response = $this
->actingAs($user)
->delete('/profile', ['password' => 'password']);
$response
->assertSessionHasNoErrors()
->assertRedirect('/');
$this->assertGuest();
$this->assertNull($user->fresh());
}
```
Key patterns:
- `$user->refresh()` to reload model from database after update
- `$user->fresh()` to check if record still exists (null after delete)
- `$this->assertGuest()` to verify logout
- `->assertSessionHasErrorsIn('userDeletion', 'password')` for error bag validation
## Fixtures and Factories
**Factory location:** `src/database/factories/UserFactory.php`
**UserFactory:**
```php
class UserFactory extends Factory
{
protected static ?string $password;
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
```
- Uses `fake()` helper for Faker data generation
- Static `$password` cached to hash only once
- `unverified()` state method for email-unverified users
- No factories exist for `station`, `rainfall`, `waterlevel`, `siren`, or `notification` tables
## Coverage
**Requirements:**
- No coverage target configured in `phpunit.xml`
- No coverage report configuration present
- No CI pipeline detected enforcing coverage thresholds
**View Coverage:**
```bash
./vendor/bin/phpunit --coverage-html coverage/
# or with --coverage-text for terminal output
```
## Test Types
**Unit Tests:**
- 1 file: `tests/Unit/ExampleTest.php`
- Single trivial assertion: `$this->assertTrue(true)`
- Extends `PHPUnit\Framework\TestCase` directly (no Laravel app boot)
- No real unit tests for services, models, or utilities exist
**Feature Tests:**
- 2 populated files + 6 empty files:
- `ExampleTest.php` — tests `/` returns 200
- `ProfileTest.php` — 5 tests covering profile page, update, email verification, delete, and wrong-password deletion
- `tests/Feature/Auth/*Test.php` — 6 files, all **empty** (zero bytes)
**E2E Tests:**
- Not used. No Laravel Dusk or Playwright detected.
## Test Coverage Gaps
**Critical gaps:**
1. **No controller tests:** `RainfallController`, `WaterLevelController`, `AdminController`, `SirenController`, `NotificationController`, `MapController`, `cctvController`, `LocaleController` — zero tests
2. **No API tests:** `Api/StationController`, `Api/AuthController`, `Api/AlertController` — zero tests
3. **No service tests:** `FcmService` — zero tests
4. **No export tests:** `HourlyRainfallExport`, `WaterLevelExport` — zero tests
5. **No middleware tests:** `AdminMiddleware`, `LocalizationMiddleware` — zero tests
6. **No model tests:** `User` model features (casts, fillable, hidden, password reset notification) — zero tests
7. **6 placeholder test files** in `tests/Feature/Auth/` are empty (created by Laravel Breeze scaffold but never populated)
## Common Patterns
**Async Testing:**
- Not applicable — Laravel is synchronous; no async patterns in tests
**Error Testing:**
```php
public function test_correct_password_must_be_provided_to_delete_account(): void
{
$user = User::factory()->create();
$response = $this
->actingAs($user)
->from('/profile')
->delete('/profile', [
'password' => 'wrong-password',
]);
$response
->assertSessionHasErrorsIn('userDeletion', 'password')
->assertRedirect('/profile');
$this->assertNotNull($user->fresh());
}
```
Patterns:
- `->from('/profile')` to set previous URL for redirect-back tests
- `assertSessionHasErrorsIn('userDeletion', 'password')` for error bag validation
- `->assertRedirect('/profile')` to check redirect destination
- `$this->assertNotNull($user->fresh())` to verify model was NOT deleted
**Authentication Testing:**
```php
$user = User::factory()->create();
$response = $this
->actingAs($user)
->get('/profile');
$response->assertOk();
```
**Session Assertions:**
- `assertSessionHasNoErrors()` — no validation errors
- `assertSessionHasErrorsIn('userDeletion', 'password')` — specific error in error bag
- Session flash checked via redirect assertion
## CI Configuration
- No CI pipeline configuration detected (no `.github/workflows/`, `.gitlab-ci.yml`, `Jenkinsfile`, etc.)
- No code quality checks enforced in CI
- No automated test running pipeline detected
---
*Testing analysis: 2026-05-28*