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*

85
Dockerfile Normal file
View File

@@ -0,0 +1,85 @@
# PHP-FPM is a FastCGI implementation for PHP.
# Read more here: https://hub.docker.com/_/php
FROM php:8.2-fpm
RUN apt-get update
# Install useful tools
RUN apt-get -y install apt-utils nano wget dialog vim
# Install system dependencies
RUN apt-get -y install --fix-missing \
apt-utils \
build-essential \
git \
curl \
libcurl4 \
libcurl4-openssl-dev \
zlib1g-dev \
libzip-dev \
zip \
libbz2-dev \
locales \
libmcrypt-dev \
libicu-dev \
libonig-dev \
libxml2-dev \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
libwebp-dev \
libxpm-dev
#RUN curl https://maatwebsite.sh
#RUN tar -xvf blalsa .
# COPY fileni kesitu
# Install PHP extensions
RUN docker-php-ext-configure gd \
--with-freetype \
--with-jpeg \
--with-webp \
--with-xpm \
&& docker-php-ext-install gd \
&& docker-php-ext-install \
exif \
pcntl \
bcmath \
ctype \
curl \
zip
# Install Postgre PDO
RUN apt-get install -y libpq-dev \
&& docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \
&& docker-php-ext-install pdo pdo_pgsql pgsql
# Install NPM
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get install -y nodejs
# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
# Install Composer
COPY --from=composer:2.3 /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Add user for laravel application
RUN groupadd -g 1000 www
RUN useradd -u 1000 -ms /bin/bash -g www www
# Copy existing application directory contents
COPY ./src /var/www/html
# Copy existing application directory permissions
COPY --chown=www:www ./src /var/www/html
# Change current user to www
USER www
# Set port for application
EXPOSE 8000

BIN
backup/sides_20260120 Normal file

Binary file not shown.

BIN
backup/sides_20260528 Normal file

Binary file not shown.

111
docker-compose.yml Normal file
View File

@@ -0,0 +1,111 @@
# https://github.com/agungprsty/laravel-postgres-with-docker.git
version: "3.9"
networks:
sides_net:
name: sides_net
external: true
services:
app:
container_name: sides-app
build:
context: .
dockerfile: Dockerfile
volumes:
- ./src:/var/www/html
depends_on:
- postgres
networks:
- sides_net
restart: unless-stopped
postgres:
container_name: sides-db
image: postgres:18.1
restart: always
volumes:
- ./docker/postgres/data:/var/lib/postgres/data
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
ports:
- "5432:5432"
networks:
- sides_net
web:
container_name: sides-web
image: nginx:stable-alpine
restart: always
ports:
- "8080:80"
- "8443:443"
volumes:
- ./src:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
networks:
- sides_net
# Database management with pgAdmin
pgadmin:
image: dpage/pgadmin4
container_name: sides-pgAdmin
environment:
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_EMAIL}
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_PASSWORD}
ports:
- "5050:80"
depends_on:
- postgres
volumes:
- ./backup:/var/lib/pgadmin/storage/tck68000_gmail.com/backup
networks:
- sides_net
restart: unless-stopped
# Database management with Adminer
adminer:
container_name: sides-adminer
image: adminer
restart: always
ports:
- "6060:8080"
depends_on:
- postgres
networks:
- sides_net
# Run with docker compose up -d
dozzle:
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 777:8080
environment:
# Uncomment to enable container actions (stop, start, restart). See https://dozzle.dev/guide/actions
- DOZZLE_ENABLE_ACTIONS=true
#
# Uncomment to allow access to container shells. See https://dozzle.dev/guide/shell
- DOZZLE_ENABLE_SHELL=true
#
# Uncomment to enable authentication. See https://dozzle.dev/guide/authentication
# - DOZZLE_AUTH_PROVIDER=simple
filebrowser:
image: gtstef/filebrowser:stable
container_name: quantum-prod
ports:
- 8900:80
user: "0:0"
restart: unless-stopped
# user: filebrowser
volumes:
- ./filebrowser-data:/home/filebrowser/data
- ./files:/files
- /root/sides:/sides # Add other sources
environment:
- "FILEBROWSER_CONFIG=data/config.yaml" # using our config file at ./data/config.yaml

BIN
docker/image/adminer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

BIN
docker/image/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docker/image/pgadmin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

35
docker/nginx/default.conf Normal file
View File

@@ -0,0 +1,35 @@
server {
listen 80;
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/html/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
charset utf-8;
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
gzip_static on;
}
location ~ /\.(?!well-known).* {
deny all;
}
}

View File

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

View File

@@ -0,0 +1 @@
{"modified":1779938995,"size":5479}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

0
sides_db.sql Normal file
View File

7
sidesuser.csv Normal file
View File

@@ -0,0 +1,7 @@
id,name,email,email_verified_at,password,remember_token,created_at,updated_at,access_level,is_blocked,login_attempts
1,admin,,,$2y$12$nhMtTY4c.23PZRLm8pGDQuQZ1vtib7VJn.iyLNzQno4wYfSks8IgW,,2025-11-07 08:25:26,2025-12-04 10:38:59,1,f,0
5,imam14,imambuhori744@gmail.com,,$2y$12$69S6G0JKD.xBYGgUZl56P.87S70itR6JRv8egkf3OYyTyBX/HF8Lm,,2025-11-13 08:00:58,2025-12-10 17:10:58,1,f,0
8,ImamBuhori,,,$2y$12$JTNbQ9A2PE0.5unVJQytt.JKAZ0ncjz/CttbyOUKm0XEZHGPhTmSe,,2025-11-14 06:44:25,2025-11-21 15:08:23,2,f,0
7,ijantck,ijantck@gmail.com,,$2y$12$Vqy1nj8gOSA7qECWpsuHkeF/5Si9v3hVuRQUTt.E6pSdtXXKc3wRG,,2025-11-14 03:35:35,2025-11-22 23:45:25,1,f,0
9,user1,,,$2y$12$yF.mHyDAycXJsjTbDKktx.no3WnyZu1Qih6.JkjXYmfVBvBrGIon.,,2025-11-24 14:26:07,2025-11-24 14:26:07,1,f,0
10,jpskedah,,,$2y$12$eVTZVk9FnC2UxCavh5vOKeXhQ/HdTnlX9WxDmKBOnvgoh7Qd3UGk.,,2025-11-24 15:37:23,2025-11-24 15:37:23,1,f,0
1 id name email email_verified_at password remember_token created_at updated_at access_level is_blocked login_attempts
2 1 admin $2y$12$nhMtTY4c.23PZRLm8pGDQuQZ1vtib7VJn.iyLNzQno4wYfSks8IgW 2025-11-07 08:25:26 2025-12-04 10:38:59 1 f 0
3 5 imam14 imambuhori744@gmail.com $2y$12$69S6G0JKD.xBYGgUZl56P.87S70itR6JRv8egkf3OYyTyBX/HF8Lm 2025-11-13 08:00:58 2025-12-10 17:10:58 1 f 0
4 8 ImamBuhori $2y$12$JTNbQ9A2PE0.5unVJQytt.JKAZ0ncjz/CttbyOUKm0XEZHGPhTmSe 2025-11-14 06:44:25 2025-11-21 15:08:23 2 f 0
5 7 ijantck ijantck@gmail.com $2y$12$Vqy1nj8gOSA7qECWpsuHkeF/5Si9v3hVuRQUTt.E6pSdtXXKc3wRG 2025-11-14 03:35:35 2025-11-22 23:45:25 1 f 0
6 9 user1 $2y$12$yF.mHyDAycXJsjTbDKktx.no3WnyZu1Qih6.JkjXYmfVBvBrGIon. 2025-11-24 14:26:07 2025-11-24 14:26:07 1 f 0
7 10 jpskedah $2y$12$eVTZVk9FnC2UxCavh5vOKeXhQ/HdTnlX9WxDmKBOnvgoh7Qd3UGk. 2025-11-24 15:37:23 2025-11-24 15:37:23 1 f 0

59
src/README.md Normal file
View File

@@ -0,0 +1,59 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development)**
- **[Active Logic](https://activelogic.com)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Exports;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
class HourlyRainfallExport implements FromCollection, WithHeadings, ShouldAutoSize
{
protected $stationid;
protected $startDate;
protected $endDate;
public function __construct($stationid,$startDate,$endDate)
{
$this->stationid = $stationid;
$this->startDate = $startDate;
$this->endDate = $endDate;
}
public function collection()
{
$hourlyColumns = [];
for ($i = 0; $i <= 23; $i++) {
$hour = sprintf("%02d", $i);
$hourlyColumns[] = "
MAX(CASE WHEN EXTRACT(HOUR FROM r.timestamp) = $i THEN r.hourly END) AS hour_$hour
";
}
$hourlyColumnSql = implode(",", $hourlyColumns);
$sql = "
SELECT
r.timestamp::date AS date,
s.stationid,
MAX(r.daily) AS total_24,
$hourlyColumnSql
FROM rainfall r
INNER JOIN station s ON r.stationid = s.stationid
WHERE r.stationid = :stationid
AND r.timestamp::date BETWEEN :startDate AND :endDate
GROUP BY r.timestamp::date, s.stationid
ORDER BY r.timestamp::date
";
$historyData = DB::select($sql, [
'stationid' => $this->stationid,
'startDate' => $this->startDate,
'endDate' => $this->endDate,
]);
return collect($historyData ?: []);
}
public function headings():array{
$headings = ['Date','Station Id','Total 24H'];
for($i = 0; $i <= 23; $i++)
{
$headings[] = sprintf('%02d:00',$i);
}
return $headings;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Exports;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
class WaterLevelExport implements FromCollection, WithHeadings, ShouldAutoSize
{
protected $stationid;
protected $startDate;
public function __construct($stationid,$startDate)
{
$this->stationid = $stationid;
$this->startDate = $startDate;
}
public function collection()
{
$sql ="
SELECT
s.stationid,
w.datetime,
w.waterlevel,
w.alert,
w.warning,
w.danger
FROM station s
INNER JOIN waterlevel w ON s.stationid = w.stationid
WHERE s.stationid = :stationid
AND w.datetime::date = :startDate
AND w.datetime = date_trunc('hour',w.datetime);
";
$historyWl = DB::select($sql,[
'stationid' => $this->stationid,
'startDate' => $this->startDate
]);
return collect($historyWl ?: []);
}
public function headings():array{
$headings = ['Station Id','Timestamp',
'Water Level','Thershold Alert','Threshold Warning','Threshold Danger'];
return $headings;
}
}

View File

@@ -0,0 +1,271 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class AdminController extends Controller
{
//Function Retrieve Station List
public function stationDisplay()
{
$stations = DB::table('station')
->select('station.*')
->orderBy('stationid')
->paginate(5);
$rainfallCount = DB::table('station')->where('rainfall', 1)->count();
$waterLevelCount = DB::table('station')->where('waterlevel', 1)->count();
$sirenCount = DB::table('station')->where('siren', 1)->count();
$stationsCount = DB::table('station')->count();
return view('layout.admin.stationmgmt', compact(
'stations',
'rainfallCount',
'waterLevelCount',
'sirenCount',
'stationsCount'
));
}
// Function Retrieve User List
public function userDisplay()
{
$users = DB::table('users')
->select('users.*')
->paginate(5);
$adminCount = DB::table('users')->where('access_level', 1)->count();
$usersCount = DB::table('users')->where('access_level', 2)->count();
$allUsersCount = DB::table('users')->count();
$latestId = DB::select('SELECT MAX(id) as max_id FROM users');
$nextId = ($latestId[0]->max_id ?? 0) + 1;
return view('layout.admin.usermgmt', compact(
'users',
'adminCount',
'usersCount',
'allUsersCount',
'nextId'
));
}
// Function Insert Station
public function storeStation(Request $request)
{
$validated = $request->validate([
'stationid' => 'required|string|max:20|unique:station,stationid',
'stationname' => 'required|string|max:255',
'district' => 'required|string|max:255',
'longitude' => 'required|numeric',
'latitude' => 'required|numeric',
'mainriverbasin' => 'nullable|string|max:255',
'subriverbasin' => 'nullable|string|max:255',
'cctv_link' => 'nullable|string|max:500',
]);
$rainfall = $request->has('rainfall') ? 1 : 0;
$waterlevel = $request->has('waterlevel') ? 1 : 0;
$siren = $request->has('siren') ? 1 : 0;
DB::table('station')->insert([
'stationid' => $validated['stationid'],
'name' => $validated['stationname'],
'district' => $validated['district'],
'lng' => $validated['longitude'],
'lat' => $validated['latitude'],
'mainriverbasin' => $validated['mainriverbasin'],
'subriverbasin' => $validated['subriverbasin'],
'rainfall' => $rainfall,
'waterlevel' => $waterlevel,
'siren' => $siren,
'cctv_link' => $validated['cctv_link'],
]);
return redirect()->back()->with('success',__('toast.stationsuccess'));
}
// Function Insert User
public function storeUser(Request $request)
{
try {
$validated = $request->validate([
'name' => 'required|string|min:5|max:255|unique:users,name',
'email' => 'nullable|string|email|max:255|unique:users,email',
'password' => 'required|string|min:6|confirmed',
'access_level' => 'required|integer', // e.g., 1 = admin, 2 = normal user
]);
DB::table('users')->insert([
'name' => $validated['name'],
'email' => $validated['email'] ?? null,
'password' => bcrypt($validated['password']),
'access_level' => $validated['access_level'],
'login_attempts' => 0,
'is_blocked' => 0,
'created_at' => now(),
'updated_at' => now(),
]);
return redirect()->back()->with('success', __('toast.usersuccess'));
} catch (ValidationException $e) {
// Get first error message
$errorMessage = collect($e->errors())->flatten()->first();
return redirect()->back()->with('error', $errorMessage);
} catch (\Exception $e) {
// For other exceptions
return redirect()->back()->with('error', $e->getMessage());
}
}
// Function update Station
public function updateStation(Request $request,$stationid)
{
$validated = $request->validate([
'stationname' => 'required|string|max:255',
'district' => 'required|string|max:255',
'longitude' => 'required|numeric',
'latitude' => 'required|numeric',
'mainriverbasin' => 'nullable|string|max:255',
'subriverbasin' => 'nullable|string|max:255',
'cctv_link' => 'nullable|string|max:500',
]);
$rainfall = $request->has('rainfall') ? 1 : 0;
$waterlevel = $request->has('waterlevel') ? 1 : 0;
$siren = $request->has('siren') ? 1 : 0;
DB::table('station')->where('stationid',$stationid)
->update([
'name' => $validated['stationname'],
'district' => $validated['district'],
'lng' => $validated['longitude'],
'lat' => $validated['latitude'],
'mainriverbasin' => $validated['mainriverbasin'],
'subriverbasin' => $validated['subriverbasin'],
'rainfall' => $rainfall,
'waterlevel' => $waterlevel,
'siren' => $siren,
'cctv_link' => $validated['cctv_link'],
]);
return redirect()->back()->with('success',__('toast.stationupdated'));
}
// Function update User
public function updateUsers(Request $request,$userid)
{
try
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'nullable|string|email|max:255',
'access_level' => 'required|integer',
]);
if ($request->has('is_blocked'))
{
DB::table('users')->where('id',$userid)
->update([
'is_blocked' => 0,
'login_attempts' => 0,
'updated_at' => now(),
]);
}else{
DB::table('users')->where('id',$userid)
->update([
'is_blocked' => 1,
'updated_at' => now(),
]);
}
DB::table('users')->where('id',$userid)
->update([
'name' => $validated['name'],
'email' => $validated['email'],
'access_level' => $validated['access_level'],
'updated_at' => now(),
]);
return redirect()->back()->with('success',__('toast.userupdated'));
}
catch (ValidationException $e) {
// Get first error message
$errorMessage = collect($e->errors())->flatten()->first();
return redirect()->back()->with('error', $errorMessage);
} catch (\Exception $e) {
// For other exceptions
return redirect()->back()->with('error', $e->getMessage());
}
}
// Function Update User Password
public function updatePassword(Request $request,$userid)
{
try{
$validated = $request->validate([
'password' => 'required|string|min:6|confirmed',
]);
DB::table('users')->where('id',$userid)
->update([
'password' => bcrypt($validated['password']),
'updated_at' => now(),
]);
return redirect()->back()->with('success',__('toast.passwordupdated'));
} 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());
}
}
// Function Delete Station
public function deleteStation($stationid)
{
DB::table('station')->where('stationid',$stationid)->delete();
return redirect()->back()->with('success',__('toast.stationdeleted'));
}
// Function Delete User
public function deleteUser ($userid)
{
DB::table('users')->where('id',$userid)->delete();
return redirect()->back()->with('success',__('toast.userdeleted'));
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\FcmService;
class AlertController extends Controller
{
protected $fcm;
public function __construct(FcmService $fcm){
$this->fcm = $fcm;
}
public function send(Request $request)
{
$stationid = $request->stationid;
$level = $request->level;
$stationtype = $request->stationtype;
// $topic = match($level){
// 'Warning' => env('FCM_TOPIC_RAINFALL_WARNING'),
// 'Danger' => env('FCM_TOPIC_RAINFALL_DANGER'),
// default => env('FCM_TOPIC_RAINFALL_WARNING')
// };
$topic = env('FCM_TOPIC_RAINFALL_WARNING');
if($stationtype == 1)
{
$station = 'Rainfall';
}
elseif($stationtype == 2)
{
$station = 'Water Level';
}
elseif ($stationtype == 3)
{
$station = 'Siren';
}
$title = "{$station} {$level} Alert";
$body = "{$stationid} : {$station} Have Triggered {$level}";
$status = $this->fcm->sendToTopic($topic,$title,$body);
return response()->json(['status'=>$status]);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function login(Request $request)
{
// Validate request
if (!$request->username || !$request->password) {
return response()->json([
'error' => true,
'message' => 'Required field are missing'
]);
}
// Query using DB::select (RAW SQL)
$user = DB::select("
SELECT id, name, email, access_level, password
FROM users
WHERE name = ?
LIMIT 1
", [$request->username]);
if (!$user) {
return response()->json([
'error' => true,
'message' => 'Wrong Password/Username'
]);
}
$user = $user[0];
// Check password using Laravel's hash system
if (!Hash::check($request->password, $user->password)) {
return response()->json([
'error' => true,
'message' => 'Wrong Password/Username'
]);
}
// Success
return response()->json([
'error' => false,
'id' => $user->id,
'username' => $user->name,
'email' => $user->email,
'acc_lvl' => $user->access_level
]);
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
class StationController extends Controller
{
public function getCurrentData()
{
$data = DB::table('station as s')
->leftJoin('rainfall as r', function($join) {
$join->on('r.stationid', '=', 's.stationid')
->whereRaw('r.timestamp = (SELECT MAX(timestamp) FROM rainfall WHERE stationid = s.stationid)');
})
->leftJoin('waterlevel as w', function($join) {
$join->on('w.stationid', '=', 's.stationid')
->whereRaw('w.datetime = (SELECT MAX(datetime) FROM waterlevel WHERE stationid = s.stationid)');
})
->leftJoin('siren as sir', function($join) {
$join->on('sir.stationid', '=', 's.stationid')
->whereRaw('sir.active_time = (SELECT MAX(active_time) FROM siren WHERE stationid = s.stationid)');
})
->whereNotNull('s.lat')
->whereNotNull('s.lng')
->where(function($query) {
$query->whereNotNull('r.hourly')
->orWhereNotNull('w.waterlevel')
->orWhereNotNull('sir.level');
})
->select(
's.*',
'r.hourly as rainfall_value',
'r.timestamp as rainfall_time',
'w.waterlevel as waterlevel_value',
'w.datetime as waterlevel_time',
'sir.level as siren_level',
'sir.active_time as siren_time'
)
->orderBy('s.stationid','asc')
->get();
return response()->json($data);
}
public function getRainfallData()
{
$dataRf = DB::table('station as s')
->leftJoin('rainfall as r', function($join) {
$join->on('r.stationid', '=', 's.stationid')
->whereRaw('r.timestamp = (
SELECT MAX(timestamp)
FROM rainfall
WHERE stationid = s.stationid
)');
})
->select(
's.*',
'r.*'
)->whereNotNull('s.rainfall')
->whereNotNull('r.stationid')
->get();
return response()->json($dataRf);
}
public function getWlData()
{
$dataWl = DB::table('station as s')
->leftJoin('waterlevel as l', function($join) {
$join->on('l.stationid', '=', 's.stationid')
->whereRaw('l.datetime = (
SELECT MAX(datetime)
FROM waterlevel
WHERE stationid = s.stationid
)');
})
->select(
's.*',
'l.*'
)->whereNotNull('s.waterlevel')
->whereNotNull('l.stationid')
->get();
return response()->json($dataWl);
}
public function getCurrentNoti()
{
$dataCurrentNoti = DB::table('station as s')
->join('notification as n', function($join) {
$join->on('s.stationid', '=', 'n.stationid');
})
->whereRaw('n.timestamp::date = CURRENT_DATE')
->whereRaw('n.timestamp = (
SELECT MAX(n2.timestamp)
FROM notification n2
WHERE n2.stationid = s.stationid
AND n2.timestamp::date = CURRENT_DATE
)')
->orderByDesc('n.timestamp')
->select('s.*', 'n.*')
->get();
return response()->json($dataCurrentNoti);
}
public function getHistory()
{
$dataHistoryNoti = DB::table('station as s')
->join('notification as n', function($join) {
$join->on('s.stationid', '=', 'n.stationid');
})
->whereRaw("n.timestamp::date >= CURRENT_DATE - INTERVAL '3 days'")
->whereRaw('(n.timestamp, n.stationid, n.timestamp::date) IN (
SELECT MAX(n2.timestamp), n2.stationid, n2.timestamp::date
FROM notification n2
WHERE n2.timestamp::date >= CURRENT_DATE - INTERVAL \'3 days\'
GROUP BY n2.stationid, n2.timestamp::date
)')
->orderByDesc('n.timestamp')
->select('s.*', 'n.*')
->get();
return response()->json($dataHistoryNoti);
}
public function getSiren()
{
$sirenData = DB::table('station')
->join('siren', 'station.stationid', '=', 'siren.stationid')
->join(DB::raw('(SELECT stationid, MAX(active_time) AS active_time FROM siren GROUP BY stationid) as latest'), function($join) {
$join->on('siren.stationid', '=', 'latest.stationid')
->on('siren.active_time', '=', 'latest.active_time');
})
->where('station.siren', 1)
->whereRaw("siren.active_time >= CURRENT_DATE - INTERVAL '3 days'")
->orderByDesc('siren.active_time')
->select('station.*', 'siren.*')
->get();
return response()->json($sirenData);
}
public function getSirenHistory()
{
$sirenHistoryData = DB::table('station')
->join('siren', 'station.stationid', '=', 'siren.stationid')
->where('station.siren', 1)
->where('siren.level',"!=","N")
->whereRaw("siren.active_time >= CURRENT_DATE - INTERVAL '3 days'")
->orderByDesc('siren.active_time')
->select('station.*', 'siren.*')
->get();
return response()->json($sirenHistoryData);
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace App\Http\Controllers\Auth;
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;
use Illuminate\Support\Facades\DB;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): View
{
$data = DB::table('station as s')
->leftJoin('rainfall as r', function($join) {
$join->on('r.stationid', '=', 's.stationid')
->whereRaw('r.timestamp = (SELECT MAX(timestamp) FROM rainfall WHERE stationid = s.stationid)');
})
->leftJoin('waterlevel as w', function($join) {
$join->on('w.stationid', '=', 's.stationid')
->whereRaw('w.datetime = (SELECT MAX(datetime) FROM waterlevel WHERE stationid = s.stationid)');
})
->leftJoin('siren as sir', function($join) {
$join->on('sir.stationid', '=', 's.stationid')
->whereRaw('sir.active_time = (SELECT MAX(active_time) FROM siren WHERE stationid = s.stationid)');
})
->whereNotNull('s.lat')
->whereNotNull('s.lng')
->where(function($query) {
$query->whereNotNull('r.hourly')
->orWhereNotNull('w.waterlevel');
})
->select(
's.*',
'r.hourly as rainfall_value',
'r.timestamp as rainfall_time',
'w.waterlevel as waterlevel_value',
'w.datetime as waterlevel_time',
'sir.level as siren_level',
'sir.active_time as siren_time'
)
->orderBy('s.stationid','asc')->get();
return view('layout.dashboard',compact('data'));
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->validate([
'name' => 'required|string',
'password' => 'required|string',
]);
$user = \App\Models\User::where('name', $request->name)->first();
// Check if user exists and is blocked
if ($user && $user->is_blocked) {
return back()->with(['error' => __('auth.accblocked')]);
}
// Attempt login
$credentials = $request->only('name', 'password');
$remember = $request->boolean('remember');
if (!Auth::attempt($credentials, $remember)) {
if ($user) {
$user->increment('login_attempts');
if ($user->login_attempts >= 3) {
$user->update(['is_blocked' => true]);
return back()->with(['error' => __('auth.blocked')]);
}
}
return back()->with(['error' => __('auth.failed')]);
}
// Reset login attempts after successful login
if ($user) {
$user->update(['login_attempts' => 0]);
}
$request->session()->regenerate();
$loggedInUser = Auth::user(); // Always use this after Auth::attempt()
return redirect()->intended('/')
->with('success', __('auth.success'). $loggedInUser->name . '.');
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/dashboard');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): View
{
return view('auth.confirm-password');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|View
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: view('auth.verify-email');
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): View
{
return view('auth.reset-password', ['request' => $request]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (User $user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $status == Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): View
{
return view('auth.forgot-password');
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
return $status == Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): View
{
return view('auth.register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Session;
class LocaleController extends Controller
{
// Function set Locale language
public function setLocale($lang)
{
if (in_array($lang,['en','bm']))
{
App::setLocale($lang);
Session::put('locale',$lang);
}
return back();
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class MapController extends Controller
{
// Function Retrieve Data From Stations Table
public function getStations()
{
$stations = DB::table('station')
->select('station.*')
->whereNotNull('lat')
->whereNotNull('lng')
->get();
return response()->json($stations);
}
// Function Retrive Current Reading For Each Stations
public function getCurrentData()
{
$data = DB::table('station as s')
->leftJoin('rainfall as r', function($join) {
$join->on('r.stationid', '=', 's.stationid')
->whereRaw('r.timestamp = (SELECT MAX(timestamp) FROM rainfall WHERE stationid = s.stationid)');
})
->leftJoin('waterlevel as w', function($join) {
$join->on('w.stationid', '=', 's.stationid')
->whereRaw('w.datetime = (SELECT MAX(datetime) FROM waterlevel WHERE stationid = s.stationid)');
})
->leftJoin('siren as sir', function($join) {
$join->on('sir.stationid', '=', 's.stationid')
->whereRaw('sir.active_time = (SELECT MAX(active_time) FROM siren WHERE stationid = s.stationid)');
})
->whereNotNull('s.lat')
->whereNotNull('s.lng')
->where(function($query) {
$query->whereNotNull('r.hourly')
->orWhereNotNull('w.waterlevel');
})
->select(
's.*',
'r.hourly as rainfall_value',
'r.timestamp as rainfall_time',
'w.waterlevel as waterlevel_value',
'w.datetime as waterlevel_time',
'sir.level as siren_level',
'sir.active_time as siren_time'
)
->orderBy('s.stationid','asc')->get();
return view('layout.dashboard',compact('data'));
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
// Add This For Import DOMPDF Library
use Barryvdh\DomPDF\Facade\Pdf;
class NotificationController extends Controller
{
//Function Retrieve Current Rainfall Notification
public function rainfallNotification()
{
$rainfallData =collect( DB::select("
SELECT s.*, n.*
FROM station s
INNER JOIN notification n ON s.stationid = n.stationid
WHERE n.stationtype = 1
AND n.timestamp::date = CURRENT_DATE
ORDER BY n.timestamp desc
"));
return view('layout.notification.rainfall',compact('rainfallData'));
}
// Function Retrieve Current Water Level Notification
public function wlNotification()
{
$wlData = collect(DB::select("
SELECT s.*,n.*
FROM station s
INNER JOIN notification n ON s.stationid = n.stationid
WHERE n.stationtype = 2
AND n.timestamp::date = CURRENT_DATE
ORDER BY timestamp desc
"));
return view('layout.notification.waterlevel',compact('wlData'));
}
// Function Retrieve Current Siren Notification
public function SirenNotification()
{
$sirenData = collect(DB::select("
SELECT
station.*,
siren.*
FROM station
INNER JOIN siren ON station.stationid = siren.stationid
INNER JOIN (
SELECT stationid, MAX(active_time) AS active_time
FROM siren
GROUP BY stationid
) latest ON siren.stationid = latest.stationid
AND siren.active_time = latest.active_time
WHERE station.siren = 1 AND siren.level != 'N' AND siren.active_time >= CURRENT_DATE - INTERVAL '3 days'
ORDER BY siren.active_time DESC
"));
return view('layout.notification.siren',compact('sirenData'));
}
// Function Retrieve Rainfall Notification History
public function rfHistory()
{
$rfHistory = DB::table('station')
->join('notification','station.stationid','notification.stationid')
->select('station.*','notification.*')
->where('notification.stationtype',1)
->orderByDesc('notification.timestamp')
->paginate(10);
return view('layout.notification.history.rainfall',compact('rfHistory'));
}
// Function Retrieve Water Level History
public function wlHistory()
{
$wlHistory = DB::table('station')
->join('notification','station.stationid','notification.stationid')
->select('station.*','notification.*')
->where('notification.stationtype',2)
->orderByDesc('notification.timestamp')
->paginate(10);
return view('layout.notification.history.waterlevel',compact('wlHistory'));
}
//Function Export PDF for Rainfall History
public function exportHistoryRfPDF()
{
$rfHistory = DB::table('station')
->join('notification','station.stationid','notification.stationid')
->select('station.*','notification.*')
->where('notification.stationtype',1)
->orderByDesc('notification.timestamp')->get();
$pdf = Pdf::loadView('pdf.rfhistory',compact('rfHistory'))
->setPaper('a4','potrait');
return $pdf->download('Rainfall Alarm History.pdf');
}
// Function Export PDF for Water Level History
public function exportHistoryWlPDF()
{
$wlHistory = DB::table('station')
->join('notification','station.stationid','notification.stationid')
->select('station.*','notification.*')
->where('notification.stationtype',2)
->orderByDesc('notification.timestamp')->get();
$pdf = Pdf::loadView('pdf.wlhistory',compact('wlHistory'))
->setPaper('a4','potrait');
return $pdf->download('Water Level Alarm History.pdf');
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): View
{
return view('profile.edit', [
'user' => $request->user(),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit')->with('status', 'profile-updated');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

View File

@@ -0,0 +1,375 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
// Add This For Export Data In Excel
use App\Exports\HourlyRainfallExport;
// Ni install manual in the container
use Maatwebsite\Excel\Facades\Excel;
class RainfallController extends Controller
{
// Function Retrieve Rainfall Data For Each Station
public function index(Request $request)
{
$stationFilter = $request->get('station');
$dateTImeFilter = $request->input('date');
$displayDate = $dateTImeFilter ? $dateTImeFilter : now();
$dateFilter = $dateTImeFilter ? Carbon::parse($dateTImeFilter)->format('Y-m-d')
:Carbon::today()->format('Y-m-d');
$stations = DB::table('station')->select('stationid', 'name')
->where('rainfall',1)
->orderBy('stationid')->get();
$stationCondition = '';
if ($stationFilter)
{
$stationCondition = " AND s.stationid = '{$stationFilter}'";
}
$rainfallData = collect(DB::select("
SELECT
s.stationid,
s.name,
s.district,
-- selected datetime filter
CAST('$displayDate' AS timestamp) AS selected_timestamp,
-- Latest hourly value
(
SELECT l2.hourly
FROM rainfall l2
WHERE l2.stationid = s.stationid
ORDER BY l2.timestamp DESC
LIMIT 1
) AS hourly,
-- Latest daily rainfall
(
SELECT l3.daily
FROM rainfall l3
WHERE l3.stationid = s.stationid
ORDER BY l3.timestamp DESC
LIMIT 1
) AS daily,
(
SELECT l4.timestamp
FROM rainfall l4
WHERE l4.stationid = s.stationid
AND DATE(l4.timestamp) <= CAST('$dateFilter' AS date)
ORDER BY l4.timestamp DESC
LIMIT 1
) AS last_updated,
-- Historical daily values for the past 7 days
MAX(CASE WHEN DATE(l.timestamp) = CAST('$dateFilter' as date) - INTERVAL '6 days' THEN l.daily END) AS day1,
MAX(CASE WHEN DATE(l.timestamp) = CAST('$dateFilter' as date) - INTERVAL '5 days' THEN l.daily END) AS day2,
MAX(CASE WHEN DATE(l.timestamp) = CAST('$dateFilter' as date) - INTERVAL '4 days' THEN l.daily END) AS day3,
MAX(CASE WHEN DATE(l.timestamp) = CAST('$dateFilter' as date) - INTERVAL '3 days' THEN l.daily END) AS day4,
MAX(CASE WHEN DATE(l.timestamp) = CAST('$dateFilter' as date) - INTERVAL '2 days' THEN l.daily END) AS day5,
MAX(CASE WHEN DATE(l.timestamp) = CAST('$dateFilter' as date) - INTERVAL '1 day' THEN l.daily END) AS day6,
MAX(CASE WHEN DATE(l.timestamp) = CAST('$dateFilter' as date) THEN l.daily END) AS day7
FROM station s
INNER JOIN rainfall l ON s.stationid = l.stationid
WHERE TO_CHAR(l.timestamp, 'HH24:MI:SS') != '00:00:00'
AND l.timestamp >= CAST('$dateFilter' as date) - INTERVAL '6 days'
$stationCondition
GROUP BY s.stationid, s.name, s.district
"
));
$lastupdate = DB::table('rainfall')->max('timestamp');
$dates = collect(range(6,1))->map(fn($i)=>Carbon::parse($dateFilter)->subDays($i)->format('d/m/Y'));
return view('layout.rainfall',compact('rainfallData','lastupdate','dates','stations','displayDate'));
}
// Function For 6 Hours Rainfall Data for Each Station For Early Warning Threshold
public function rainfallSum(Request $request)
{
$stationFilter = $request->input('station');
$dateFilter = $request->input('date');
// If no date submitted → use current local time
$displayDate = $dateFilter ? $dateFilter : now('Asia/Kuala_Lumpur');
// Convert to Y-m-d H:i:s for SQL comparison
$sqlDate = \Carbon\Carbon::parse($displayDate)->format('Y-m-d H:i:s');
// Fetch stations
$stations = DB::table('station')->select('stationid', 'name')->where('rainfall',1)->orderBy('stationid')->get();
// Build SQL query
$sql = "
WITH latest AS (
SELECT
s.stationid,
s.name,
s.district,
MAX(l.timestamp) AS latest_timestamp
FROM station s
INNER JOIN rainfall l ON s.stationid = l.stationid
WHERE 1=1
";
$bindings = [];
// Apply date filter
if ($dateFilter) {
$sql .= " AND l.timestamp = :dateFilter ";
$bindings['dateFilter'] = $sqlDate;
}
$sql .= "
GROUP BY s.stationid, s.name, s.district
),
hourly_intervals AS (
SELECT
s.stationid,
s.name,
s.district,
l.currentrf,
l.timestamp,
EXTRACT(EPOCH FROM (lt.latest_timestamp - l.timestamp)) / 3600 AS hours_diff,
lt.latest_timestamp
FROM station s
INNER JOIN rainfall l ON s.stationid = l.stationid
INNER JOIN latest lt ON lt.stationid = s.stationid
WHERE l.timestamp <= lt.latest_timestamp
AND l.timestamp > lt.latest_timestamp - INTERVAL '6 hours'
)
SELECT
stationid,
name,
district,
MAX(CASE WHEN hours_diff < 1 THEN currentrf END) AS hour1_value,
MAX(CASE WHEN hours_diff < 1 THEN TO_CHAR(timestamp, 'HH24:MI:SS') END) AS hour1_time,
MAX(CASE WHEN hours_diff >= 1 AND hours_diff < 2 THEN currentrf END) AS hour2_value,
MAX(CASE WHEN hours_diff >= 1 AND hours_diff < 2 THEN TO_CHAR(timestamp, 'HH24:MI:SS') END) AS hour2_time,
MAX(CASE WHEN hours_diff >= 2 AND hours_diff < 3 THEN currentrf END) AS hour3_value,
MAX(CASE WHEN hours_diff >= 2 AND hours_diff < 3 THEN TO_CHAR(timestamp, 'HH24:MI:SS') END) AS hour3_time,
MAX(CASE WHEN hours_diff >= 3 AND hours_diff < 4 THEN currentrf END) AS hour4_value,
MAX(CASE WHEN hours_diff >= 3 AND hours_diff < 4 THEN TO_CHAR(timestamp, 'HH24:MI:SS') END) AS hour4_time,
MAX(CASE WHEN hours_diff >= 4 AND hours_diff < 5 THEN currentrf END) AS hour5_value,
MAX(CASE WHEN hours_diff >= 4 AND hours_diff < 5 THEN TO_CHAR(timestamp, 'HH24:MI:SS') END) AS hour5_time,
MAX(CASE WHEN hours_diff >= 5 AND hours_diff < 6 THEN currentrf END) AS hour6_value,
MAX(CASE WHEN hours_diff >= 5 AND hours_diff < 6 THEN TO_CHAR(timestamp, 'HH24:MI:SS') END) AS hour6_time,
MAX(latest_timestamp) AS last_update
FROM hourly_intervals
WHERE 1=1
";
// Apply station filter
if ($stationFilter) {
$sql .= " AND stationid = :stationFilter ";
$bindings['stationFilter'] = $stationFilter;
}
$sql .= " GROUP BY stationid, name, district ORDER BY stationid;";
// Execute query
$thresholdData = collect(DB::select($sql, $bindings));
$lastupdate = DB::table('rainfall')->max('timestamp');
return view('layout.threshold', compact('thresholdData', 'stations', 'displayDate','lastupdate'));
}
// Function retrieve Hourly rainfall Graph For Each Station
public function rainfallGraph($stationid)
{
$graphData = DB::table('rainfall')
->select(DB::raw("TO_CHAR(timestamp, 'HH24:MI') AS hour"), DB::raw("CASE
WHEN TO_CHAR(timestamp, 'HH24:MI') LIKE '00:%'
THEN 0
ELSE hourly
END AS hourly") )
->whereDate('timestamp', today())
->whereRaw("EXTRACT(MINUTE FROM timestamp) = 0")
->where('stationid', $stationid)
->orderBy('timestamp')
->get();
return response()->json($graphData);
// $graphData = DB::table('rainfall')
// ->select(
// DB::raw("DATE(timestamp) as date"),
// DB::raw("TO_CHAR(timestamp, 'HH24:MI') AS time_slot"),
// 'currentrf'
// )
// ->where('stationid', $stationid)
// ->whereBetween('timestamp', [
// now()->subDays(7)->startOfDay(),
// now()->endOfDay()
// ])
// ->whereRaw("EXTRACT(MINUTE FROM timestamp) % 5 = 0") // only multiples of 5 minutes
// ->orderBy('timestamp')
// ->get();
}
// Function retrieve data of 6 Hours Rainfall fo Graph Page
public function graphData($stationid,$dates)
{
$dates = urldecode($dates);
$latest = DB::table('rainfall')
->where('stationid', $stationid)
->where('timestamp',$dates)
->max('timestamp');
if (!$latest) {
// no data in table
return response()->json([
'labels' => [],
'data' => []
]);
}
// 2. Generate 6 hourly timestamps counting back from latest
$labels = [];
$times = [];
$latestCarbon = Carbon::parse($latest);
for ($i = 5; $i >= 0; $i--) {
$ts = $latestCarbon->copy()->subHours($i);
$labels[] = $ts->format('H:i'); // chart label
$times[] = $ts; // for query matching
}
// 3. Query rainfall data for these 6 timestamps
$graphData = DB::table('rainfall')
->select(
DB::raw("TO_CHAR(timestamp, 'HH24:MI') as time"),
'hourly'
)
->where('stationid', $stationid)
->whereBetween('timestamp', [
$latestCarbon->copy()->subHours(5),
$latestCarbon
])
->orderBy('timestamp')
->get();
// 4. Map data to labels, fill missing hours with 0
$data = [];
foreach ($labels as $label) {
$record = $graphData->firstWhere('time', $label);
$data[] = $record ? $record->hourly : 0;
}
return response()->json([
'labels' => $labels,
'data' => $data
]);
}
//Return View Graph Page
public function graphPage($stationid,$dates)
{
// $station = DB::table('station')->where('stationid',$stationid)->first();
return view('layout.graph.rainfall',compact('stationid','dates'));
}
// Function for Retrieve Historical Rainfall Data
public function historicalRainfall(Request $request)
{
$stationFilter = $request->get('station');
$startDateInput = $request->input('startdate');
$endDateInput = $request->input('enddate');
$displayDate = $startDateInput ?: now();
$displayEndDate = $endDateInput ?: now();
$stations = DB::table('station')
->select('stationid', 'name')
->where('rainfall', 1)
->orderBy('stationid')
->get();
$startDate = Carbon::parse($startDateInput)->toDateString();
$endDate = Carbon::parse($endDateInput)->toDateString();
// Build the hourly columns without referencing grouped outer query
$hourlyColumns = [];
for ($i = 0; $i <= 23; $i++) {
$hour = sprintf("%02d", $i);
$hourlyColumns[] = "
MAX(CASE WHEN EXTRACT(HOUR FROM timestamp) = $i THEN hourly END) AS hour_$hour
";
}
$hourlyColumnSql = implode(",", $hourlyColumns);
// Use a CTE to get latest per hour per day first
$sql = "
WITH latest_per_hour AS (
SELECT DISTINCT ON (stationid, timestamp::date, EXTRACT(HOUR FROM timestamp))
stationid,
timestamp,
hourly,
daily
FROM rainfall
WHERE stationid = :stationid
AND timestamp::date BETWEEN :startDate AND :endDate
ORDER BY stationid, timestamp::date, EXTRACT(HOUR FROM timestamp), timestamp DESC
)
SELECT
timestamp::date AS date,
stationid,
$hourlyColumnSql,
MAX(daily) FILTER (WHERE EXTRACT(HOUR FROM timestamp) <> 0) AS total_24
FROM latest_per_hour
GROUP BY timestamp::date, stationid
ORDER BY timestamp::date
";
$historyData = collect(DB::select($sql, [
'stationid' => $stationFilter,
'startDate' => $startDate,
'endDate' => $endDate
]));
return view('layout.historicalrainfall', compact(
'historyData',
'stations',
'displayDate',
'displayEndDate'
));
}
// Function for export Historical Rainfall To Excel File
public function exportHourlyRainfallExcel(Request $request)
{
$stationid = $request->get('station');
$startDate = $request->input('startdate');
$endDate = $request->input('enddate');
$startDate2 = Carbon::parse($startDate)->toDateString();
$endDate2 = Carbon::parse($endDate)->toDateString();
return Excel::download(
new HourlyRainfallExport($stationid,$startDate,$endDate),"Hourly Rainfall {$stationid} {$startDate2} - {$endDate2}.xlsx"
);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Barryvdh\DomPDF\Facade\Pdf;
class SirenController extends Controller
{
// Function retrieve Current Siren Statuss
public function index()
{
$sirenData = collect(DB::select("
SELECT
station.*,
siren.*
FROM station
INNER JOIN siren ON station.stationid = siren.stationid
INNER JOIN (
SELECT stationid, MAX(active_time) AS active_time
FROM siren
GROUP BY stationid
) latest ON siren.stationid = latest.stationid
AND siren.active_time = latest.active_time
WHERE station.siren = 1 AND siren.active_time >= CURRENT_DATE - INTERVAL '3 days'
ORDER BY siren.active_time DESC
"));
return view('layout.siren.home',compact('sirenData'));
}
// Function retrieve Threshold Triggered Siren History
public function SirenHistory()
{
$historyData = DB::table('station')
->join('siren','station.stationid','siren.stationid')
->select('station.*','siren.*')
->where('siren.level', '!=', 'N')
->where('siren.level', '!=', '')
->where('station.siren',1)
->orderByDesc('siren.active_time')
->paginate(10);
return view('layout.siren.history',compact('historyData'));
}
// Function for download Siren History to PDF
public function exportHistorySirenPDF()
{
$sirenHistory = DB::table('station')
->join('siren','station.stationid','siren.stationid')
->select('station.*','siren.*')
->where('siren.level', '!=', 'N')
->where('siren.level', '!=', '')
->where('station.siren',1)
->orderByDesc('siren.active_time')
->get();
$pdf = Pdf::loadView('pdf.sirenhistory',compact('sirenHistory'))
->setPaper('a4','potrait');
return $pdf->download(
'Station_Siren_History.pdf',
[],
['Content-Type' => 'application/pdf']
);
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
// Add this for Export data to excel
use App\Exports\WaterLevelExport;
use Maatwebsite\Excel\Facades\Excel;
class WaterLevelController extends Controller
{
// Function Retrieve Current Water Level Data
public function index(Request $request)
{
$stationFilter = $request->get('station');
$dateFilter = $request->input('date');
$displayDate = $dateFilter ? $dateFilter : now();
$sqlDate = \Carbon\Carbon::parse($displayDate)->format('Y-m-d H:i:s');
$stationCondition = '';
$dateCondition = '';
if($stationFilter)
{
$stationCondition = " WHERE s.stationid = '{$stationFilter}' ";
}
if ($dateFilter) {
$dateCondition = " AND w.datetime = '{$sqlDate}' ";
} else {
$dateCondition = "
AND w.datetime = (
SELECT MAX(datetime)
FROM waterlevel w2
WHERE w2.stationid = s.stationid
)
";
}
$wldata =collect(DB::select("
SELECT s.*, w.*
FROM station s
INNER JOIN waterlevel w
ON s.stationid = w.stationid
$stationCondition
$dateCondition
ORDER BY s.name ASC
"));
$stations = DB::table('station')->select('stationid', 'name')
->where('waterlevel',1)
->orderBy('stationid')->get();
return view('layout.waterlevel',compact('wldata','stations','displayDate'));
}
// Function Retrieve Daily Water Level Data for water level graph
public function graphData($stationid)
{
$graphData = DB::table('waterlevel')
->select(DB::raw("TO_CHAR(datetime, 'HH24:MI') as hour"),'waterlevel',
'alert','warning','danger')
->whereDate('datetime', today())
->whereRaw("EXTRACT(MINUTE FROM datetime) = 0")
->where('stationid',$stationid)
->orderBy('datetime')->get();
return response()->json($graphData);
}
// Function Retrieve Water Level Historical Data
public function wlHistory(Request $request)
{
$stationid = $request->get('station');
$startDate = $request->input('startdate');
$stations = DB::table('station')->select('stationid','name')
->where('waterlevel',1)
->orderBy('stationid')->get();
$displayDate = $startDate ? $startDate : now();
$sql ="
SELECT
s.stationid,
s.name,
w.datetime::date AS date,
w.datetime::time AS time,
w.waterlevel,
w.alert,
w.warning,
w.danger
FROM station s
INNER JOIN waterlevel w ON s.stationid = w.stationid
WHERE s.stationid = :stationid
AND w.datetime::date = :startDate
AND w.datetime = date_trunc('hour',w.datetime);
";
$historyWl = collect(
DB::select($sql,[
'stationid' => $stationid,
'startDate' => $startDate
])
);
return view('layout.historicalwl',compact('historyWl','displayDate','stations'));
}
// Function export historical water level data
public function exportHistoricalWl(Request $request)
{
$stationid = $request->get('station');
$startDate = $request->input('startdate');
$startDate2 = Carbon::parse($startDate)->toDateString();
return Excel::download(
new WaterLevelExport($stationid,$startDate),"Water Level {$stationid} {$startDate2}.xlsx"
);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class cctvController extends Controller
{
// Function Display Station CCTV LINK
public function index()
{
// $stationdata = DB::select("
// SELECT s.*, w.*
// FROM station s
// INNER JOIN waterlevel w
// ON s.stationid = w.stationid
// INNER JOIN (
// SELECT stationid, MAX(datetime) AS latest_datetime
// FROM waterlevel
// GROUP BY stationid
// ) latest ON w.stationid = latest.stationid
// AND w.datetime = latest.latest_datetime
// WHERE s.cctv_link IS NOT NULL
// ORDER BY s.name ASC
// ");
$stationdata = DB::select("
SELECT name,district,cctv_link FROM station
WHERE cctv_link IS NOT NULL AND waterlevel = 1
ORDER BY name ASC
");
return view('layout.cctv',compact('stationdata'));
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Auth;
class AdminMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (!Auth::check())
{
return redirect('/dashboard')->with('error','Please Log In First');
}
if(Auth::user()->access_level !== 1)
{
return redirect('/dashboard')->with('error','Unauthorized Access');
}
return $next($request);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\App;
use Symfony\Component\HttpFoundation\Response;
class LocalizationMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$locale = Session::get('locale') ?? 'en';
Session::put('locale',$locale);
App::setlocale($locale);
return $next($request);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string'],
'password' => ['required', 'string'],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('name', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'name' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'name' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('name')).'|'.$this->ip());
}
public function username():string
{
return 'name';
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
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),
],
];
}
}

59
src/app/Models/User.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use App\Notifications\ResetPasswordNotification;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
'access_level',
'login_attempts',
'is_blocked',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_blocked' => 'boolean',
];
}
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetPasswordNotification($token));
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ResetPasswordNotification extends Notification
{
use Queueable;
/**
* Create a new notification instance.
*/
public function __construct(String $token)
{
$this->token = $token;
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('SIDES - Password Reset')
->greeting('Hello,')
->line('We received a request to reset the password for your SIDES account.')
->line('Click the button below to set a new password')
->action('Reset Password', url(route('password.reset',['token' => $this->token,'email'=> $notifiable->email],false)))
->line('This password reset link will expire in 60 minutes.')
->line('If you did not request this, you can safely ignore this email.')
->salutation('Regards, SIDES Team'); // ✅ FOOTER TEXT
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\URL;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// URL::forceScheme('https');
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Services;
use Google\Auth\Credentials\ServiceAccountCredentials;
use Illuminate\Support\Facades\Http;
class FcmService
{
protected $projectid;
protected $credentials;
public function __construct()
{
$this->projectid = env('FIREBASE_PROJECT_ID');
$this->credentials = json_decode(
file_get_contents(base_path(env('FIREBASE_CREDENTIALS'))),
true
);
}
public function sendToTopic(string $topic ,string $title,string $body)
{
$scopes = ['https://www.googleapis.com/auth/firebase.messaging'];
// $accessToken = ApplicationDefaultCredentials::getAccessToken(
// $scopes,
// $this->credentials
// );
$creds = new ServiceAccountCredentials($scopes, $this->credentials);
$tokenArray = $creds->fetchAuthToken();
$accessToken = $tokenArray['access_token'] ?? null;
if (!$accessToken) {
throw new \Exception("Failed to get access token from Firebase credentials.");
}
$response = Http::withToken($accessToken)
->post("https://fcm.googleapis.com/v1/projects/{$this->projectid}/messages:send", [
"message" => [
"topic" => $topic,
"notification" => [
"title" => $title,
"body" => $body
],
"android" => [
"priority" => "high"
]
]
]);
return $response->status();
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class AppLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.app');
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class GuestLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.guest');
}
}

18
src/artisan Normal file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);

26
src/bootstrap/app.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\AdminMiddleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'admin' => AdminMiddleware::class,
]);
$middleware->web(append:[
App\Http\Middleware\LocalizationMiddleware::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();

74
src/bootstrap/cache/packages.php vendored Normal file
View File

@@ -0,0 +1,74 @@
<?php return array (
'barryvdh/laravel-dompdf' =>
array (
'aliases' =>
array (
'PDF' => 'Barryvdh\\DomPDF\\Facade\\Pdf',
'Pdf' => 'Barryvdh\\DomPDF\\Facade\\Pdf',
),
'providers' =>
array (
0 => 'Barryvdh\\DomPDF\\ServiceProvider',
),
),
'laravel/breeze' =>
array (
'providers' =>
array (
0 => 'Laravel\\Breeze\\BreezeServiceProvider',
),
),
'laravel/pail' =>
array (
'providers' =>
array (
0 => 'Laravel\\Pail\\PailServiceProvider',
),
),
'laravel/sail' =>
array (
'providers' =>
array (
0 => 'Laravel\\Sail\\SailServiceProvider',
),
),
'laravel/tinker' =>
array (
'providers' =>
array (
0 => 'Laravel\\Tinker\\TinkerServiceProvider',
),
),
'maatwebsite/excel' =>
array (
'aliases' =>
array (
'Excel' => 'Maatwebsite\\Excel\\Facades\\Excel',
),
'providers' =>
array (
0 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
),
),
'nesbot/carbon' =>
array (
'providers' =>
array (
0 => 'Carbon\\Laravel\\ServiceProvider',
),
),
'nunomaduro/collision' =>
array (
'providers' =>
array (
0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
),
),
'nunomaduro/termwind' =>
array (
'providers' =>
array (
0 => 'Termwind\\Laravel\\TermwindServiceProvider',
),
),
);

267
src/bootstrap/cache/services.php vendored Normal file
View File

@@ -0,0 +1,267 @@
<?php return array (
'providers' =>
array (
0 => 'Illuminate\\Auth\\AuthServiceProvider',
1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
2 => 'Illuminate\\Bus\\BusServiceProvider',
3 => 'Illuminate\\Cache\\CacheServiceProvider',
4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
5 => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
6 => 'Illuminate\\Cookie\\CookieServiceProvider',
7 => 'Illuminate\\Database\\DatabaseServiceProvider',
8 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
9 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
10 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
11 => 'Illuminate\\Hashing\\HashServiceProvider',
12 => 'Illuminate\\Mail\\MailServiceProvider',
13 => 'Illuminate\\Notifications\\NotificationServiceProvider',
14 => 'Illuminate\\Pagination\\PaginationServiceProvider',
15 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
16 => 'Illuminate\\Pipeline\\PipelineServiceProvider',
17 => 'Illuminate\\Queue\\QueueServiceProvider',
18 => 'Illuminate\\Redis\\RedisServiceProvider',
19 => 'Illuminate\\Session\\SessionServiceProvider',
20 => 'Illuminate\\Translation\\TranslationServiceProvider',
21 => 'Illuminate\\Validation\\ValidationServiceProvider',
22 => 'Illuminate\\View\\ViewServiceProvider',
23 => 'Barryvdh\\DomPDF\\ServiceProvider',
24 => 'Laravel\\Breeze\\BreezeServiceProvider',
25 => 'Laravel\\Pail\\PailServiceProvider',
26 => 'Laravel\\Sail\\SailServiceProvider',
27 => 'Laravel\\Tinker\\TinkerServiceProvider',
28 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
29 => 'Carbon\\Laravel\\ServiceProvider',
30 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
31 => 'Termwind\\Laravel\\TermwindServiceProvider',
32 => 'App\\Providers\\AppServiceProvider',
),
'eager' =>
array (
0 => 'Illuminate\\Auth\\AuthServiceProvider',
1 => 'Illuminate\\Cookie\\CookieServiceProvider',
2 => 'Illuminate\\Database\\DatabaseServiceProvider',
3 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
4 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
5 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
6 => 'Illuminate\\Notifications\\NotificationServiceProvider',
7 => 'Illuminate\\Pagination\\PaginationServiceProvider',
8 => 'Illuminate\\Session\\SessionServiceProvider',
9 => 'Illuminate\\View\\ViewServiceProvider',
10 => 'Barryvdh\\DomPDF\\ServiceProvider',
11 => 'Laravel\\Pail\\PailServiceProvider',
12 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
13 => 'Carbon\\Laravel\\ServiceProvider',
14 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
15 => 'Termwind\\Laravel\\TermwindServiceProvider',
16 => 'App\\Providers\\AppServiceProvider',
),
'deferred' =>
array (
'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Contracts\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Contracts\\Bus\\QueueingDispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Bus\\BatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Bus\\DatabaseBatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
'cache' => 'Illuminate\\Cache\\CacheServiceProvider',
'cache.store' => 'Illuminate\\Cache\\CacheServiceProvider',
'cache.psr6' => 'Illuminate\\Cache\\CacheServiceProvider',
'memcached.connector' => 'Illuminate\\Cache\\CacheServiceProvider',
'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider',
'Illuminate\\Foundation\\Console\\AboutCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\ForgetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ClearCompiledCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Auth\\Console\\ClearResetsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\DbCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\PruneCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\ShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\WipeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\DownCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentDecryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentEncryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Concurrency\\Console\\InvokeSerializedClosureCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\KeyGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\OptimizeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\OptimizeClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\PackageDiscoverCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\PruneStaleTagsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ListFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\FlushFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\DumpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Seeds\\SeedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleFinishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleRunCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleClearCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleTestCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleWorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleInterruptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\ShowModelCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StorageLinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StorageUnlinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\UpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ApiInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\BroadcastingInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\CacheTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\CastMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ChannelListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ChannelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ClassMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ComponentMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConsoleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Routing\\Console\\ControllerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\DocsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnumMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ExceptionMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Factories\\FactoryMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\InterfaceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\JobMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\JobMiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\LangPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ListenerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\MailMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Routing\\Console\\MiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ModelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\NotificationMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Notifications\\Console\\NotificationTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ObserverMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\PolicyMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ProviderMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\FailedTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\BatchesTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RequestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ResourceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RuleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ScopeMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Seeds\\SeederMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Session\\Console\\SessionTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ServeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StubPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\TestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\TraitMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\VendorPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migration.repository' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migration.creator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Migrations\\Migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\MigrateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\FreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\InstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\RefreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\ResetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\RollbackCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\StatusCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\MigrateMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'composer' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Concurrency\\ConcurrencyManager' => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
'hash' => 'Illuminate\\Hashing\\HashServiceProvider',
'hash.driver' => 'Illuminate\\Hashing\\HashServiceProvider',
'mail.manager' => 'Illuminate\\Mail\\MailServiceProvider',
'mailer' => 'Illuminate\\Mail\\MailServiceProvider',
'Illuminate\\Mail\\Markdown' => 'Illuminate\\Mail\\MailServiceProvider',
'auth.password' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
'auth.password.broker' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
'Illuminate\\Contracts\\Pipeline\\Hub' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
'pipeline' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
'queue' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.connection' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.failer' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.listener' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.worker' => 'Illuminate\\Queue\\QueueServiceProvider',
'redis' => 'Illuminate\\Redis\\RedisServiceProvider',
'redis.connection' => 'Illuminate\\Redis\\RedisServiceProvider',
'translator' => 'Illuminate\\Translation\\TranslationServiceProvider',
'translation.loader' => 'Illuminate\\Translation\\TranslationServiceProvider',
'validator' => 'Illuminate\\Validation\\ValidationServiceProvider',
'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider',
'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider',
'Laravel\\Breeze\\Console\\InstallCommand' => 'Laravel\\Breeze\\BreezeServiceProvider',
'Laravel\\Sail\\Console\\InstallCommand' => 'Laravel\\Sail\\SailServiceProvider',
'Laravel\\Sail\\Console\\PublishCommand' => 'Laravel\\Sail\\SailServiceProvider',
'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider',
),
'when' =>
array (
'Illuminate\\Broadcasting\\BroadcastServiceProvider' =>
array (
),
'Illuminate\\Bus\\BusServiceProvider' =>
array (
),
'Illuminate\\Cache\\CacheServiceProvider' =>
array (
),
'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider' =>
array (
),
'Illuminate\\Concurrency\\ConcurrencyServiceProvider' =>
array (
),
'Illuminate\\Hashing\\HashServiceProvider' =>
array (
),
'Illuminate\\Mail\\MailServiceProvider' =>
array (
),
'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider' =>
array (
),
'Illuminate\\Pipeline\\PipelineServiceProvider' =>
array (
),
'Illuminate\\Queue\\QueueServiceProvider' =>
array (
),
'Illuminate\\Redis\\RedisServiceProvider' =>
array (
),
'Illuminate\\Translation\\TranslationServiceProvider' =>
array (
),
'Illuminate\\Validation\\ValidationServiceProvider' =>
array (
),
'Laravel\\Breeze\\BreezeServiceProvider' =>
array (
),
'Laravel\\Sail\\SailServiceProvider' =>
array (
),
'Laravel\\Tinker\\TinkerServiceProvider' =>
array (
),
),
);

View File

@@ -0,0 +1,5 @@
<?php
return [
App\Providers\AppServiceProvider::class,
];

90
src/composer.json Normal file
View File

@@ -0,0 +1,90 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.2",
"barryvdh/laravel-dompdf": "^3.1",
"google/auth": "^1.49",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"maatwebsite/excel": "^3.1"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/breeze": "^2.3",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^11.5.3"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"setup": [
"composer install",
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"npm install",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"test": [
"@php artisan config:clear --ansi",
"@php artisan test"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

9535
src/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

126
src/config/app.php Normal file
View File

@@ -0,0 +1,126 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => 'Asia/Kuala_Lumpur',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];

115
src/config/auth.php Normal file
View File

@@ -0,0 +1,115 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the number of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];

117
src/config/cache.php Normal file
View File

@@ -0,0 +1,117 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "octane",
| "failover", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
'failover' => [
'driver' => 'failover',
'stores' => [
'database',
'array',
],
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
];

183
src/config/database.php Normal file
View File

@@ -0,0 +1,183 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
'transaction_mode' => 'DEFERRED',
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
],
];

View File

@@ -0,0 +1,80 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem disks as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];

132
src/config/logging.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
|
| Available drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'handler_with' => [
'stream' => 'php://stderr',
],
'formatter' => env('LOG_STDERR_FORMATTER'),
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

118
src/config/mail.php Normal file
View File

@@ -0,0 +1,118 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
*/
'default' => env('MAIL_MAILER', 'log'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
'retry_after' => 60,
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
'retry_after' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all emails sent by your application to be sent from
| the same address. Here you may specify a name and address that is
| used globally for all emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
];

129
src/config/queue.php Normal file
View File

@@ -0,0 +1,129 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue supports a variety of backends via a single, unified
| API, giving you convenient access to each backend using identical
| syntax for each. The default queue connection is defined below.
|
*/
'default' => env('QUEUE_CONNECTION', 'database'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Laravel. You're also free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
| "deferred", "background", "failover", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
'deferred' => [
'driver' => 'deferred',
],
'background' => [
'driver' => 'background',
],
'failover' => [
'driver' => 'failover',
'connections' => [
'database',
'deferred',
],
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
];

38
src/config/services.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'key' => env('POSTMARK_API_KEY'),
],
'resend' => [
'key' => env('RESEND_API_KEY'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
];

217
src/config/session.php Normal file
View File

@@ -0,0 +1,217 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option determines the default session driver that is utilized for
| incoming requests. Laravel supports a variety of storage options to
| persist session data. Database storage is a great default choice.
|
| Supported: "file", "cookie", "database", "memcached",
| "redis", "dynamodb", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env(
'SESSION_COOKIE',
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application, but you're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain and all subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
];

Binary file not shown.

View File

@@ -0,0 +1,44 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
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),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}

View File

@@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration');
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('station', function (Blueprint $table) {
$table->string('stationid')->primary();
$table->string('name');
$table->string('district');
$table->float('lng');
$table->float('lat');
$table->string('mainriverbasin');
$table->string('subriverbasin');
$table->integer('rainfall');
$table->integer('waterlevel');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('station');
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('rainfall', function (Blueprint $table) {
$table->id();
$table->string('stationid');
$table->timestamp('timestamp');
$table->double('anncum');
$table->double('daily');
$table->double('hourly');
$table->double('currentrf');
$table->double('battery');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('rainfall');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('waterlevel', function (Blueprint $table) {
$table->id();
$table->string('stationid');
$table->timestamp('datetime');
$table->double('waterlevel');
$table->double('alert');
$table->double('warning');
$table->double('danger');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('waterlevel');
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('notification', function (Blueprint $table) {
$table->id();
$table->string('stationid');
$table->timestamp('timestamp');
$table->integer('stationtype');
$table->string('level');
$table->timestamp('active_time')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('notification');
}
};

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('siren', function (Blueprint $table) {
$table->id();
$table->string('stationid');
$table->string('stationtype');
$table->timestamp('active_time');
$table->string('level');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('siren');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->integer('access_level')->default(2)->after('email');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('access_level');
});
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_blocked')->default(false);
$table->integer('login_attempts')->default(0);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_blocked');
$table->dropColumn('login_attempts');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('station', function (Blueprint $table) {
$table->integer('siren')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('station', function (Blueprint $table) {
$table->dropColumn('siren');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('station', function (Blueprint $table) {
$table ->string('cctv_link')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('station', function (Blueprint $table) {
$table->dropColumn('cctv_link');
});
}
};

View File

@@ -0,0 +1,29 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::create([
'name' => 'Admin',
'email' => 'admin@example.com',
'password' => Hash::make('password123'),
'access_level' => 1,
]);
}
}

23
src/lang/bm/auth.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'Nama Pengguna Atau Kata Laluan Salah',
'blocked' => 'Terlalu banyak percubaan yang gagal. Akaun disekat',
'accblocked' => 'Akaun anda disekat. Hubungi pentadbir sistem',
'success' => 'Log Masuk Berjaya! Selamat Datang ',
'password' => 'The provided password is incorrect.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
];

140
src/lang/bm/messages.php Normal file
View File

@@ -0,0 +1,140 @@
<?php
return [
'home' => 'Laman Utama',
'notification' => 'Notifikasi',
'rainfall' => 'Hujan',
'wl' => 'Aras Air',
'ewt' => 'Ambang Amaran Awal',
'otherlink' => 'Pautan Lain',
'daily' => 'Harian',
'dailywl' => 'Aras Air Harian',
'dailyrf' => 'Hujan Harian',
'notificationhistory' => 'Sejarah Notifikasi',
'totalrf' => 'Jumlah Hujan',
'historicalrf' => 'Sejarah Hujan',
'historicalwl' => 'Sejarah Aras Air',
'stationmgmt' => 'Pengurusan Stesen',
'usermgmt' => 'Pengurusan Pengguna',
'login' => 'Log Masuk',
'logout' => 'Log Keluar',
'close' => ' Tutup',
'from' => 'daripada',
'rfstation' => 'Stesen Hujan',
'wlstation' => 'Stesen Aras Air',
'sirenstation' => 'Stesen Siren',
'user' => 'Pengguna',
'other' => 'Lain-lain',
'today' => 'Harian',
//Dashboard Elements
'stationdata' => 'Data Stesen',
'name' => 'Nama',
'updated' => 'Kemas Kini',
'infotitle' => 'Apa itu SIDES ?',
'info' => 'SABO INTEGRATED DEBRIS FLOWS MONITORING AND EARLY WARNING SYSTEM (SIDES) merupakan sebahagian daripada inisiatif tebatan aliran banjir dan serpihan berkaitan pembinaan Empangan Sabo di Sg Kupang, Baling. Sistem ini melibatkan pemasangan stesen hujan, stesen paras air, sistem pengesan aliran lumpur, siren, dan CCTV.',
// Table Elements
'station' => 'Stesen',
'date' => 'Tarikh',
'time' => 'Masa',
'title' => 'Tajuk',
'description' => 'Penerangan',
'activeat' => 'Aktif Pada',
'rfalarm' => 'Penggera Hujan',
'triggerd' => 'Tercapai',
'rfat' => 'Hujan pada tahap',
'norf'=>'Tiada Notifikasi Hujan Untuk Hari Ini',
'nowl'=>'Tiada Notifikasi Aras Air Untuk Hari Ini',
'nosiren'=>'Tiada Siren Aktif Untuk Hari Ini',
'wlexceed' => 'Paras Air telah melebihi paras',
'level' => 'Tahap',
'dateandtime'=> 'Tarikh Dan Masa',
'rfsincemidnight' => 'Hujan Sejak Tengah Malam',
'graph' => 'Graf',
'lastupdate' => 'Kemaskini Terakhir',
'district' => 'Daerah',
'total24' => 'Jumlah 24 Jam',
'mainbasin' => 'Lembangan Sungai Utama',
'subbasin' => 'Lembangan Bawah Sungai ',
'sixhourrf' => 'Hujan 6 Jam',
'sixhourrfavg' => 'Purata Hujan 6 Jam',
'link' => 'Pautan',
'location' => 'Lokasi',
'action' => 'Tindakan',
//Threshold
'threshold' => 'Ambang',
'danger' => 'Bahaya',
'warning' => 'Amaran',
'normal' => 'Normal',
'heavy' => 'Lebat',
'veryheavy' => 'Sangat Lebat',
'avgheavy' => 'Purata Lebat',
'avgveryheavy' => 'Purata Sangat Lebat',
//Button
'export' => 'Eksport',
'search' => 'Cari',
'addstation' => 'Tambah Stesen',
'adduser' => 'Tambah Pengguna',
'delete' => 'Padam',
'cancel' => 'Batal',
'save' => 'Simpan',
'Yes' => 'Ya',
'update' => 'Kemas Kini',
//No Data Messages
'nohistoryrf' => 'Tiada Data Sejarah Notifikasi Hujan',
'nohistorywl' => 'Tiada Data Sejarah Notifikasi Aras Air',
'nohistorysiren' => 'Tiada Data Sejarah Siren',
'nocurrentsiren' => 'Tiada siren aktif sejak 7 hari lalu',
'nodataavailable' => 'Tiada Data Tersedia',
//Form
'selectstation' => 'Pilih Stesen',
'allstation' => 'Semua Stesen',
'startdate' => 'Tarikh Mula',
'enddate' => 'Tarikh Tamat',
//Station Form
'stationinfo' => 'Butiran Stesen',
'stationname' => 'Nama Stesen',
'stationtype' => 'Jenis Stesen',
'delconfirmation' => 'Pengesahan Padam',
'deldes' => 'Adakah anda pasti mahu memadam ',
//User Form
'userinfo' => 'Butiran Pengguna',
'username' => 'Nama Pengguna',
'email' => 'Emel',
'position' => 'Jawatan',
'changepass' => 'Tukar Kata Laluan',
'accstatus' => 'Status Akaun',
'selposition' => 'Pilih Jawatan',
'password' => 'Kata laluan',
'repassword' => 'Kata laluan Semula',
'admin' => 'Pentadbir Sistem',
'active' => 'Aktif',
'block' => 'Disekat',
'newpassword' => 'Kata Laluan Baharu',
'confirmpassword' => 'Sahkan Kata Laluan',
'forgotpassword' => 'Lupa Kata Laluan',
//ewt
'datarequirement' => 'Keperluan Data',
'ewtdes' => 'Adalah disyorkan untuk mengambil masa 6 jam daripada lengkung untuk menilai titik pencetus dan definisi peristiwa baharu jika berterusan selama 6 jam, hujan kurang daripada 15 mm/jam',
//PDF Title
'rfalarmhistory' =>'Sejarah Penggera Hujan',
'wlalarmhistory' =>'Sejarah Penggera Aras',
'sirenhistory' =>'Sejarah Siren',
'generated' =>'Dijana pada',
];
?>

View File

@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Previous',
'next' => 'Next &raquo;',
];

22
src/lang/bm/passwords.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| outcome such as failure due to an invalid password / reset token.
|
*/
'reset' => 'Your password has been reset.',
'sent' => 'We have emailed your password reset link.',
'throttled' => 'Please wait before retrying.',
'token' => 'This password reset token is invalid.',
'user' => "We can't find a user with that email address.",
];

19
src/lang/bm/toast.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
return [
//Toast Messages
//Add
'stationsuccess' => 'Stesen berjaya ditambah',
'usersuccess' => 'Pengguna berjaya ditambah',
//Update
'stationupdated' => 'Stesen berjaya dikemaskini',
'userupdated' => 'Pengguna berjaya dikemaskini',
'passwordupdated' => 'Kata laluan berjaya dikemaskini',
//Delete
'stationdeleted' => 'Stesen berjaya dipadam',
'userdeleted' => 'Pengguna berjaya dipadam',
];
?>

Some files were not shown because too many files have changed in this diff Show More