419 lines
18 KiB
Markdown
419 lines
18 KiB
Markdown
<!-- generated-by: gsd-doc-writer -->
|
|
|
|
# Architecture
|
|
|
|
## System Overview
|
|
|
|
SIDES (Sistem Informasi Data dan Early Warning System) is a Laravel 12 web application that collects, stores, and displays real-time environmental monitoring data from rainfall, water level, and siren stations. It provides a dashboard with a map view of station status, historical data with export capabilities (Excel, PDF), and push notifications via Firebase Cloud Messaging (FCM). The system runs entirely in Docker containers using nginx as a reverse proxy, PHP-FPM for application code, and PostgreSQL 16 for persistence. External IoT stations submit readings through a public REST API; authenticated admin users manage stations and user accounts through a Blade-based web UI.
|
|
|
|
## Container Architecture
|
|
|
|
```
|
|
+------------------+
|
|
| User Browser |
|
|
+--------+---------+
|
|
|
|
|
| :80
|
|
v
|
|
+--------+---------+
|
|
| web (nginx) |
|
|
| nginx:stable- |
|
|
| alpine |
|
|
+--------+---------+
|
|
|
|
|
fastcgi :9000
|
|
|
|
|
+--------+---------+
|
|
./src -----> | app (php-fpm) |
|
|
(bind mount) | php:8.2-fpm |
|
|
+--------+---------+
|
|
|
|
|
pgsql :5432
|
|
|
|
|
+--------+---------+ +------------------+
|
|
| postgres | | pgadmin |
|
|
| postgres:16 | | dpage/pgadmin4 |
|
|
+------------------+ +------------------+
|
|
volume: pgdata volume: pgadmin_data
|
|
```
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
UB["User Browser"]:::client -->|:80| N["web (nginx)\nnginx:stable-alpine"]:::server
|
|
N -->|fastcgi :9000| A["app (php-fpm)\nphp:8.2-fpm\n(bind mount ./src)"]:::app
|
|
A -->|pgsql :5432| DB["postgres\npostgres:16\nvolume: pgdata"]:::db
|
|
PG["pgAdmin\ndpage/pgadmin4\nvolume: pgadmin_data"]:::tool --> DB
|
|
|
|
classDef client fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef server fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef app fill:#00BCD4,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef db fill:#673AB7,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef tool fill:#FFC107,stroke:#333,stroke-width:2px,color:#000;
|
|
|
|
```
|
|
|
|
### Container Summary
|
|
|
|
| Container | Image | Port | Purpose |
|
|
|------------|---------------------|--------|--------------------------------------------|
|
|
| tckdev-app | php:8.2-fpm (custom)| 9000 | Laravel application (PHP-FPM) |
|
|
| tckdev-db | postgres:16 | 5432 | PostgreSQL database |
|
|
| tckdev-web | nginx:stable-alpine | 80 | Reverse proxy, serves static assets |
|
|
| tckdev-pgAdmin | dpage/pgadmin4 | 5050 | Database administration UI |
|
|
|
|
### Networking and Volumes
|
|
|
|
- All containers share the `tckdev_net` bridge network.
|
|
- `src/` is bind-mounted into both `app` and `web` at `/var/www/html`, so code changes are reflected without rebuilding.
|
|
- `pgdata` and `pgadmin_data` are Docker named volumes -- not bind mounts.
|
|
- The nginx config is bind-mounted from `docker/nginx/default.conf`.
|
|
- `docker/backup` is bind-mounted into pgadmin at `/backups`.
|
|
|
|
### Entrypoint Behaviour
|
|
|
|
The `app` container runs `docker/entrypoint.sh` before starting PHP-FPM. This script:
|
|
|
|
1. Copies `.env.example` to `.env` if no `.env` exists.
|
|
2. Runs `composer install` if `vendor/` is missing.
|
|
3. Runs `npm install` if `node_modules/` is missing.
|
|
4. Runs `npm run build` (Vite) if `public/build/` is missing.
|
|
5. Runs `php artisan migrate --force` when `RUN_MIGRATIONS=true`.
|
|
6. Runs `php artisan db:seed --force` when `RUN_SEEDER=true`.
|
|
7. Caches config, routes, and views for production performance.
|
|
|
|
## Request Flow
|
|
|
|
### Web UI Request (Blade pages)
|
|
|
|
```
|
|
Browser --:80--> nginx
|
|
|
|
|
+-- static asset? --> /var/www/html/public/{file}
|
|
|
|
|
+-- *.php? ---------> fastcgi://app:9000
|
|
|
|
|
Laravel Router
|
|
|
|
|
+-- auth middleware --> Breeze session auth
|
|
+-- admin middleware --> access_level === 1 check
|
|
|
|
|
Controller
|
|
|
|
|
DB::table() / DB::select()
|
|
|
|
|
v
|
|
PostgreSQL
|
|
|
|
|
Blade view / JSON response
|
|
```
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
B["Browser"]:::client -->|:80| N["nginx"]:::server
|
|
|
|
N -->|static asset?| SA["/var/www/html/public/{file}"]:::static
|
|
N -->|*.php?| P["fastcgi://app:9000"]:::php
|
|
|
|
P --> R["Laravel Router"]:::router
|
|
|
|
R --> AM["auth middleware\n(Breeze session auth)"]:::middleware
|
|
R --> ADM["admin middleware\n(access_level === 1 check)"]:::middleware
|
|
R --> C["Controller"]:::controller
|
|
|
|
C --> Q["DB::table() / DB::select()"]:::query --> DB["PostgreSQL"]:::db
|
|
DB --> V["Blade view / JSON response"]:::response
|
|
|
|
classDef client fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef server fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef static fill:#9E9E9E,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef php fill:#00BCD4,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef router fill:#9C27B0,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef middleware fill:#FFC107,stroke:#333,stroke-width:2px,color:#000;
|
|
classDef controller fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef query fill:#795548,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef db fill:#673AB7,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef response fill:#607D8B,stroke:#333,stroke-width:2px,color:#fff;
|
|
```
|
|
|
|
1. Browser hits port 80 on the `web` container.
|
|
2. nginx serves static files from `/var/www/html/public` directly.
|
|
3. PHP requests are forwarded via FastCGI to `app:9000`.
|
|
4. Laravel routes the request through middleware (`auth`, `admin`, or none).
|
|
5. Controllers query PostgreSQL using `DB::table()` or `DB::select()` with parameterized bindings.
|
|
6. Responses are rendered Blade views (HTML) or JSON for AJAX endpoints.
|
|
|
|
### API Request (IoT station data)
|
|
|
|
```
|
|
IoT Device --:80/api/...--> nginx ---> app:9000
|
|
|
|
|
Laravel API Router (no auth middleware on data endpoints)
|
|
|
|
|
Controller (Api\*)
|
|
|
|
|
DB::table() / DB::select()
|
|
|
|
|
v
|
|
PostgreSQL
|
|
```
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
D["IoT Device"]:::device -->|:80/api/...| N["nginx"]:::server
|
|
N --> P["app:9000 (php-fpm)"]:::php
|
|
P --> R["Laravel API Router\n(no auth middleware on data endpoints)"]:::router
|
|
R --> C["Controller (Api\\*)"]:::controller
|
|
C --> Q["DB::table() / DB::select()"]:::query --> DB["PostgreSQL"]:::db
|
|
|
|
classDef device fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef server fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef php fill:#00BCD4,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef router fill:#9C27B0,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef controller fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef query fill:#795548,stroke:#333,stroke-width:2px,color:#fff;
|
|
classDef db fill:#673AB7,stroke:#333,stroke-width:2px,color:#fff;
|
|
```
|
|
|
|
|
|
API routes under `/api/` have no middleware. The `POST /api/login` endpoint validates credentials against the `users` table using `DB::select()` with bound parameters and `Hash::check()`. There are no API tokens -- authentication is a single request/response returning user data on success.
|
|
|
|
## Data Model
|
|
|
|
The application uses six database tables. There are no foreign key constraints; relationships are enforced at the application level. `User` is the only Eloquent model. All other data access uses the `DB` query builder with parameterized queries.
|
|
|
|
```
|
|
+----------------+ +----------------+ +----------------+
|
|
| station | | rainfall | | waterlevel |
|
|
|----------------| |----------------| |----------------|
|
|
| stationid (PK) |<--+---| id (PK) | +----| id (PK) |
|
|
| name | | | stationid | | | stationid |
|
|
| district | | | timestamp | | | datetime |
|
|
| lng | | | anncum | | | waterlevel |
|
|
| lat | | | daily | | | alert |
|
|
| mainriverbasin | | | hourly | | | warning |
|
|
| subriverbasin | | | currentrf | | | danger |
|
|
| rainfall (flag)| | | battery | | +----------------+
|
|
| waterlevel (f) | | +----------------+ |
|
|
| siren (flag) | | |
|
|
| cctv_link | | +----------------+ | +----------------+
|
|
+----------------+ | | siren | | | notification |
|
|
+---| id (PK) | +---| id (PK) |
|
|
| stationid | | stationid |
|
|
| stationtype | | timestamp |
|
|
| active_time | | stationtype |
|
|
| level | | level |
|
|
+----------------+ | active_time |
|
|
+----------------+
|
|
+----------------+
|
|
| users |
|
|
|----------------|
|
|
| id (PK) |
|
|
| name |
|
|
| email |
|
|
| access_level |
|
|
| password |
|
|
| login_attempts |
|
|
| is_blocked |
|
|
+----------------+
|
|
```
|
|
|
|
```mermaid
|
|
erDiagram
|
|
station {
|
|
int stationid
|
|
string name
|
|
string district
|
|
float lng
|
|
float lat
|
|
string mainriverbasin
|
|
string subriverbasin
|
|
boolean rainfall_flag
|
|
boolean waterlevel_flag
|
|
boolean siren_flag
|
|
string cctv_link
|
|
}
|
|
|
|
rainfall {
|
|
int id
|
|
int stationid
|
|
datetime timestamp
|
|
float anncum
|
|
float daily
|
|
float hourly
|
|
float currentrf
|
|
float battery
|
|
}
|
|
|
|
waterlevel {
|
|
int id
|
|
int stationid
|
|
datetime datetime
|
|
float waterlevel
|
|
string alert
|
|
string warning
|
|
string danger
|
|
}
|
|
|
|
siren {
|
|
int id
|
|
int stationid
|
|
string stationtype
|
|
datetime active_time
|
|
string level
|
|
}
|
|
|
|
notification {
|
|
int id
|
|
int stationid
|
|
datetime timestamp
|
|
string stationtype
|
|
string level
|
|
datetime active_time
|
|
}
|
|
|
|
users {
|
|
int id
|
|
string name
|
|
string email
|
|
string access_level
|
|
string password
|
|
int login_attempts
|
|
boolean is_blocked
|
|
}
|
|
|
|
station ||--o{ rainfall : "has"
|
|
station ||--o{ waterlevel : "has"
|
|
station ||--o{ siren : "has"
|
|
station ||--o{ notification : "has"
|
|
```
|
|
|
|
|
|
### Table Descriptions
|
|
|
|
| Table | Purpose |
|
|
|---------------|--------------------------------------------------------|
|
|
| `station` | Master list of monitoring stations with location and capability flags |
|
|
| `rainfall` | Timestamped rainfall readings per station |
|
|
| `waterlevel` | Timestamped water level readings with alert/warning/danger thresholds |
|
|
| `siren` | Siren activation events with level (N/D/W/A) |
|
|
| `notification`| Notification log entries linking stations to alert levels |
|
|
| `users` | Application users; `access_level=1` for admin |
|
|
|
|
## Application Layer
|
|
|
|
### Controllers
|
|
|
|
**Web controllers** (render Blade views):
|
|
|
|
| Controller | Responsibility |
|
|
|-------------------------|-------------------------------------------------------|
|
|
| `MapController` | Dashboard/map view; joins station with latest readings |
|
|
| `RainfallController` | Rainfall display, graphs, historical data, Excel export |
|
|
| `WaterLevelController` | Water level display, graphs, historical data, Excel export |
|
|
| `SirenController` | Siren status display, history, PDF export |
|
|
| `NotificationController`| Rainfall/water level/siren notification views and PDF exports |
|
|
| `AdminController` | Station and user CRUD (admin-only) |
|
|
| `cctvController` | CCTV feed display |
|
|
| `LocaleController` | Language switching |
|
|
| `ProfileController` | User profile management (Breeze) |
|
|
| Auth controllers | Login, register, password reset (Breeze scaffolding) |
|
|
|
|
**API controllers** (return JSON):
|
|
|
|
| Controller | Responsibility |
|
|
|-------------------------|-------------------------------------------------------|
|
|
| `Api\StationController` | Current data, rainfall, water level, notifications, siren status for IoT/external consumption |
|
|
| `Api\AuthController` | Token-less login; validates username/password, returns user JSON |
|
|
| `Api\AlertController` | Routes FCM push notifications to the correct topic based on stationtype and level |
|
|
|
|
### Middleware
|
|
|
|
| Middleware | Behavior |
|
|
|-------------------------|-------------------------------------------------------|
|
|
| `auth` | Redirects unauthenticated users to login (Breeze) |
|
|
| `admin` | Checks `access_level === 1`; redirects non-admins to dashboard |
|
|
| `LocalizationMiddleware`| Sets application locale from session |
|
|
|
|
### Services
|
|
|
|
| Service | Responsibility |
|
|
|---------------|--------------------------------------------------------------|
|
|
| `FcmService` | Obtains Google OAuth2 access token from Firebase service account credentials, sends FCM v1 API messages to topics |
|
|
|
|
### FCM Topic Routing
|
|
|
|
`AlertController::send()` determines the FCM topic from the request's `stationtype` and `level`:
|
|
|
|
| stationtype | Level | FCM Topic |
|
|
|-------------|----------|---------------------------|
|
|
| 1 | Warning | `FCM_TOPIC_RAINFALL_WARNING` |
|
|
| 1 | Danger | `FCM_TOPIC_RAINFALL_DANGER` |
|
|
| 2 | Warning | `FCM_TOPIC_WATERLEVEL_ALERT` |
|
|
| 2 | Danger | `FCM_TOPIC_WATERLEVEL_DANGER` |
|
|
| 3 | (any) | `FCM_TOPIC_RAINFALL_WARNING` |
|
|
|
|
## Authentication
|
|
|
|
Two separate authentication mechanisms coexist:
|
|
|
|
- **Web UI**: Laravel Breeze provides session-based authentication (login, register, password reset, email verification). The `auth` middleware guards protected routes.
|
|
- **API**: `POST /api/login` accepts `username` and `password`, validates against the `users` table, and returns user data as JSON. There is no token or session persisted for API consumers -- each API data endpoint is stateless and unauthenticated.
|
|
|
|
## Frontend Stack
|
|
|
|
- Blade templates with Tailwind CSS (via `tailwind.config.js`).
|
|
- Vite handles asset bundling (`vite.config.js`, `postcss.config.js`).
|
|
- Map rendering uses Leaflet with station markers.
|
|
- Client-side JavaScript in `public/js/` handles graph rendering and AJAX calls.
|
|
|
|
## Database Configuration
|
|
|
|
- **Connection**: `pgsql` (PostgreSQL 16) -- configured in `.env` as `DB_CONNECTION=pgsql`.
|
|
- **Host**: `postgres` (Docker service name).
|
|
- **Session driver**: `database` (Laravel's database session storage).
|
|
- **Cache store**: `database`.
|
|
- **Queue connection**: `database`.
|
|
|
|
All session, cache, and queue data lives in PostgreSQL tables created by Laravel's default migrations (`cache`, `jobs`, `sessions` tables).
|
|
|
|
## Directory Structure
|
|
|
|
```
|
|
tckdev/
|
|
docker/ Docker configuration
|
|
entrypoint.sh App container startup script
|
|
nginx/
|
|
default.conf nginx reverse proxy config
|
|
src/ Laravel application (bind-mounted into containers)
|
|
app/
|
|
Http/
|
|
Controllers/ Web controllers
|
|
Controllers/Api/ API controllers (StationController, AuthController, AlertController)
|
|
Middleware/ AdminMiddleware, LocalizationMiddleware
|
|
Requests/ Form request validation
|
|
Exports/ Maatwebsite Excel export classes
|
|
Models/ User (only Eloquent model)
|
|
Notifications/ ResetPasswordNotification
|
|
Services/ FcmService
|
|
Providers/ AppServiceProvider
|
|
View/Components/ AppLayout, GuestLayout (Blade components)
|
|
bootstrap/
|
|
config/ Laravel config files
|
|
database/
|
|
migrations/ Schema migrations (13 files)
|
|
seeders/ Database seeders
|
|
factories/ Model factories
|
|
public/ Document root (served by nginx)
|
|
resources/
|
|
views/ Blade templates
|
|
routes/
|
|
web.php Web routes (authenticated + admin + public)
|
|
api.php API routes (stateless, no auth middleware)
|
|
auth.php Breeze auth routes
|
|
console.php Artisan commands
|
|
storage/ App storage (logs, firebase credentials, uploads)
|
|
tests/ PHPUnit tests
|
|
docker-compose.yml Service definitions (Compose v2, no version key)
|
|
Dockerfile Custom php:8.2-fpm image
|
|
```
|