first init
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
|||||||
|
# Ignore everything
|
||||||
|
/*
|
||||||
|
|
||||||
|
# But keep these folders
|
||||||
|
!/be/
|
||||||
|
!/fe/
|
||||||
|
!/SUTERA-server-credential.txt/
|
||||||
|
!/TODO.md
|
||||||
|
!/README.md
|
||||||
|
!/redis-config
|
||||||
|
!/.gitea
|
||||||
|
!/my-kopkb.code-workspace
|
||||||
|
!/docs
|
||||||
|
|
||||||
|
# Also keep .gitignore itself
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# SUTERA 3.0 – Guide
|
||||||
|
|
||||||
|
Welcome to **SUTERA 3.0**, the streamlined training deployment setup using **Docker**, **Laravel**, and **Vite**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Login Credentials
|
||||||
|
|
||||||
|
> Use the following to log into the system:
|
||||||
|
|
||||||
|
- **Email**: `isml.msrn11@gmail.com`
|
||||||
|
- **Password**: `TOPAZTHEGOAT`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Formula Used:
|
||||||
|
1. KEUPAYAAN (%) = (PEGANGAN/PERJAWATAN) * 100
|
||||||
|
2. SIAPSIAGA or KESIAGAAN (%) = (BDG/perjawatan) * 100
|
||||||
|
3. SERVISIBILITI (%) = (BDG/pegangan) * 100
|
||||||
|
|
||||||
|
## Laravel command to clear
|
||||||
|
```bash
|
||||||
|
sail artisan config:clear
|
||||||
|
sail artisan cache:clear
|
||||||
|
sail artisan permission:cache-reset
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🎯 Addtional Notes to consider
|
||||||
|
- Make sure `/var/www/public/build` exists inside your PHP-FPM container (handled by Dockerfile).
|
||||||
|
- Assets are built via Vite in the **Node.js stage** and injected into the Laravel `public/` directory.
|
||||||
|
- Use `entrypoint.sh` for Laravel setup like `php artisan migrate` or `storage:link` if needed.
|
||||||
|
- 'DEVELOPER' roles can bypassed all permissions by default and cannot be deleted even through database
|
||||||
|
- Use delete in controller not directly in routes because no authorization and validation
|
||||||
|
- Use Gate for non-model-specific permissions. general-purpose or simple logic,
|
||||||
|
- Use policy when need structured authorization for CRUD operations. anything tied to a model/resource
|
||||||
|
- Servisibility always higher than siapsiaga
|
||||||
|
- Solution — put specific routes first before resource routes
|
||||||
|
- to use soft delete, make id and deleted_at as unique
|
||||||
|
- to make new module, run:
|
||||||
|
```bash
|
||||||
|
sail artisan module:make Example
|
||||||
|
```
|
||||||
|
- There are two User model files
|
||||||
|
|
||||||
|
## PKJ Default Varian (DO NOT DELETE) or update. If require update, be sure to update the export name for each PKJ report:
|
||||||
|
- BANTUAN MOBILITI
|
||||||
|
- BEKALAN AIR
|
||||||
|
- EOD/IED/PEMUSNAHAN
|
||||||
|
- JURUUKUR
|
||||||
|
- KENDERAAN JENIS A
|
||||||
|
- KENDERAAN JENIS B
|
||||||
|
- OPTRONIK DAN PERALATAN LATIHAN
|
||||||
|
- PELURU DAN BAHAN LETUPAN
|
||||||
|
- PEMUSNAHAN
|
||||||
|
- PERALATAN KETUKANGAN
|
||||||
|
- PERALATAN KOMUNIKASI PERTAHANAN
|
||||||
|
- PERALATAN PD&P
|
||||||
|
- PERALATAN PERAIRAN
|
||||||
|
- PERALATAN PNBK
|
||||||
|
- SENJATA PERTAHANAN
|
||||||
|
|
||||||
|
|
||||||
|
```sql
|
||||||
|
--- KJC TO UPDATE PEGANGAN IN PERJAWATAN TABLE; CHECK WILL BE UPDATED FIRST
|
||||||
|
SELECT
|
||||||
|
ae.id,
|
||||||
|
ae.entitlement,
|
||||||
|
ae.holding AS current_holding,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM kjc_asset_holdings ah
|
||||||
|
WHERE ah.kjc_category_id = ae.kjc_category_id
|
||||||
|
AND ah.kjc_category_id = ae.kjc_category_id
|
||||||
|
AND ah.unit_id = ae.unit_id
|
||||||
|
AND ah.deleted_at IS NULL
|
||||||
|
) AS actual_holding_count
|
||||||
|
FROM kjc_asset_entitlements ae
|
||||||
|
WHERE ae.deleted_at IS NULL;
|
||||||
|
|
||||||
|
--- EXECUTE IT
|
||||||
|
UPDATE kjc_asset_entitlements ae
|
||||||
|
SET ae.holding = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM kjc_asset_holdings ah
|
||||||
|
WHERE ah.kjc_category_id = ae.kjc_category_id
|
||||||
|
AND ah.kjc_category_id = ae.kjc_category_id
|
||||||
|
AND ah.unit_id = ae.unit_id
|
||||||
|
AND ah.deleted_at IS NULL
|
||||||
|
)
|
||||||
|
WHERE ae.deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run specific laravel migration:
|
||||||
|
- If it is fresh, then run:
|
||||||
|
```bash
|
||||||
|
sail artisan migrate --path=database/migrations/2025_07_30_044653_create_historical_pkj_perjawatan_table.php
|
||||||
|
```
|
||||||
|
|
||||||
|
- If not, rollback first:
|
||||||
|
```bash
|
||||||
|
sail artisan migrate:rollback --step=1
|
||||||
|
|
||||||
|
sail artisan migrate --path=database/migrations/2025_07_30_044708_create_historical_pkj_pegangan_status_table.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## To remove data in postgres and restart numbering
|
||||||
|
```bash
|
||||||
|
TRUNCATE TABLE kjc_spare_parts RESTART IDENTITY;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reset query numbering
|
||||||
|
```sql
|
||||||
|
SELECT pg_get_serial_sequence('"permissions"', 'id') AS seq_name;
|
||||||
|
SELECT last_value FROM public.permissions_id_seq;
|
||||||
|
SELECT setval('public.permissions_id_seq', COALESCE((SELECT MAX(id) FROM permissions), 0) + 1, false);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run capture data command
|
||||||
|
```bash
|
||||||
|
# kjc
|
||||||
|
sail artisan kjc:historical:trigger
|
||||||
|
|
||||||
|
# pkj
|
||||||
|
sail artisan pkj:historical:trigger
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auto-generate weekly KJC report (Jabatan Arah RAJD) - Scheduled to run every Monday at 3:00 AM.
|
||||||
|
```bash
|
||||||
|
# Manual trigger
|
||||||
|
sail artisan kjc:auto-generate-weekly-report "Jabatan Arah RAJD"
|
||||||
|
|
||||||
|
# Force regenerate even if reports exist
|
||||||
|
sail artisan kjc:auto-generate-weekly-report "Jabatan Arah RAJD" --force
|
||||||
|
```
|
||||||
|
|
||||||
|
# Git
|
||||||
|
## DO NOT RUN THIS BECAUSE IT WILL REMOVE ALL THE UNTAGGED FILES IN LOCAL
|
||||||
|
```bash
|
||||||
|
git reset --hard development # moves the current branch (e.g., main) to point exactly where development
|
||||||
|
```
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
## Fix
|
||||||
|
|
||||||
|
## Bug
|
||||||
|
|
||||||
|
## Improvement
|
||||||
|
[ ] mesyuarat anggota tertinggi
|
||||||
|
[ ] penyata anggota
|
||||||
|
[ ] daftar anggota
|
||||||
|
[ ] baki pinjaman - transaction history: tarik dari ubs
|
||||||
|
[ ] pembiayaan anggota
|
||||||
|
[ ] sumbangan
|
||||||
|
[ ] wasi/penama
|
||||||
|
[ ] pendaftaran anggota/meneruskan anggota/pencen
|
||||||
|
[ ] daftar lembaga (backdated)
|
||||||
|
[ ] boleh print semua borang
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/docker_images
|
||||||
|
/docker-images
|
||||||
|
/.vscode
|
||||||
|
.env
|
||||||
|
NOTE.md
|
||||||
|
/node_modules
|
||||||
|
/vendor
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
/bootstrap/cache
|
||||||
|
/supporting_documents
|
||||||
|
/docs
|
||||||
|
TODO.md
|
||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
deploy-production.sh
|
||||||
|
deploy.sh
|
||||||
|
deploy-staging.sh
|
||||||
|
deploy-training.sh
|
||||||
|
build.sh
|
||||||
|
TODO.md
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[docker-compose.yml]
|
||||||
|
indent_size = 4
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
APP_NAME="SUTERA 3.0 Dev"
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=base64:0dM5HYEFRu9P6ni51QTUZm5mQ23znc/s/aebLTRUGxU=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_TIMEZONE=Asia/Kuala_Lumpur
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=pgsql
|
||||||
|
DB_HOST=pgsql
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_DATABASE=sutera
|
||||||
|
DB_USERNAME=suterauser
|
||||||
|
DB_PASSWORD=password
|
||||||
|
|
||||||
|
SESSION_DRIVER=redis
|
||||||
|
SESSION_LIFETIME=60
|
||||||
|
SESSION_ENCRYPT=true
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=redis
|
||||||
|
CACHE_PREFIX=sutera_cache
|
||||||
|
|
||||||
|
MEMCACHED_HOST=memcached
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=mailpit
|
||||||
|
MAIL_PORT=1025
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
# VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||||
|
# VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||||
|
# VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||||
|
# VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||||
|
# VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||||
|
|
||||||
|
EXTERNAL_API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibWJzcC1yZXNvdXJjZUlkIl0sInRpbWVvdXRQZXJpb2QiOjkwMCwidXNlcl9uYW1lIjoic3BhLXVzZXIiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNzI4NTgzNjEyLCJqdGkiOiJjMmU2OTkxOS0yZDU3LTQ5YmYtYTVmZS1lZjVmMzIyNDZkZDIiLCJjbGllbnRfaWQiOiJzcGEtY2xpZW50In0.LE2byaRzsYkchzI4ox9Gz2u6jNmTqZQa-_ttR2PY8e8"
|
||||||
|
EXTERNAL_API_BASE_URL=http://20.11.32.134/rest/td/gasset/asset
|
||||||
|
|
||||||
|
SSO_SECRET="Y6g#Rb@Km=Nz]o^X+fEC~<W/LP%qh;M}9T&v!["
|
||||||
|
SSO_VALIDATION_URL=http://host.docker.internal:8080/api/sso-validate
|
||||||
|
SSO_MAX_ATTEMPTS=5
|
||||||
|
SSO_TOKEN_EXPIRATION=300
|
||||||
|
SSO_AUTO_ACTIVATE_USERS=true
|
||||||
|
SSO_LOG_ACTIVITIES=true
|
||||||
|
SSO_CONNECTION_TIMEOUT=10
|
||||||
|
SSO_RESPONSE_TIMEOUT=30
|
||||||
|
SSO_PROXY_URL=http://20.9.91.1:3128
|
||||||
|
SSO_ENABLE_FALLBACK=true
|
||||||
|
SSO_PROXY_ENABLED=false
|
||||||
|
|
||||||
|
SANCTUM_STATEFUL_DOMAINS=http://localhost
|
||||||
|
|
||||||
|
BLOCK_API_TOOLS_IN_PRODUCTION=true
|
||||||
|
MIN_USER_AGENT_LENGTH=15
|
||||||
|
API_BLOCKED_MESSAGE="API access is restricted in production environment. Please use the web interface."
|
||||||
|
API_ALLOWED_IPS=
|
||||||
|
API_ALLOWED_USER_AGENTS=
|
||||||
|
|
||||||
|
ENABLE_API_KEY_AUTH=true
|
||||||
|
API_VALID_KEYS=86f58825e5c1e0872d8786092d47e3bd,6461fc5390660c55dc47bdfcd85ed9ae
|
||||||
|
API_LOG_KEY_USAGE=true
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
APP_NAME=Laravel
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=sqlite
|
||||||
|
# DB_HOST=127.0.0.1
|
||||||
|
# DB_PORT=3306
|
||||||
|
# DB_DATABASE=sutera
|
||||||
|
# DB_USERNAME=suterauser
|
||||||
|
# DB_PASSWORD=
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
# Per-job timeout for Horizon/queue workers (seconds). Long-running jobs (e.g. SPATD sync) need 3600 (60 min).
|
||||||
|
# HORIZON_WORKER_TIMEOUT=3600
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
# HttpOnly Sanctum token cookie (set on api.* domain)
|
||||||
|
AUTH_COOKIE_NAME=auth_token
|
||||||
|
AUTH_COOKIE_LIFETIME=720
|
||||||
|
AUTH_COOKIE_SECURE=true
|
||||||
|
AUTH_COOKIE_SAME_SITE=lax
|
||||||
|
AUTH_COOKIE_EXPOSE_TOKEN=false
|
||||||
|
|
||||||
|
# Active role (context is stored on roles.context in DB)
|
||||||
|
ACTIVE_ROLE_PREFER_MEMBER=true
|
||||||
|
ACTIVE_ROLE_MEMBER_REDIRECT=/profile
|
||||||
|
ACTIVE_ROLE_ADMIN_REDIRECT=/profile
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
APP_NAME="SUTERA 3.0"
|
||||||
|
APP_ENV=production
|
||||||
|
APP_KEY=base64:XdMAy5IbQMHsTrp1N7Tn/LIJC4SKenN8CfhYLt1s3tk=
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_TIMEZONE=Asia/Kuala_Lumpur
|
||||||
|
APP_URL=http://20.11.32.50
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
PHP_CLI_SERVER_WORKERS=8
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=error
|
||||||
|
|
||||||
|
DB_CONNECTION=pgsql
|
||||||
|
DB_HOST=20.11.32.56
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_DATABASE=sutera
|
||||||
|
DB_USERNAME=suterauser
|
||||||
|
DB_PASSWORD=Aretus@Production_2025
|
||||||
|
|
||||||
|
SESSION_DRIVER=redis
|
||||||
|
SESSION_LIFETIME=60
|
||||||
|
SESSION_ENCRYPT=true
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=redis
|
||||||
|
|
||||||
|
HORIZON_WORKER_MEMORY=512
|
||||||
|
HORIZON_WORKER_TIMEOUT=3600
|
||||||
|
|
||||||
|
CACHE_STORE=redis
|
||||||
|
CACHE_PREFIX=sutera_cache
|
||||||
|
CACHE_DRIVER=redis
|
||||||
|
|
||||||
|
MEMCACHED_HOST=memcached
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=sutera_redis@2025
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
|
# only change this to smtp if is deployed to public server
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=mailpit
|
||||||
|
MAIL_PORT=1025
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
# VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||||
|
# VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||||
|
# VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||||
|
# VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||||
|
# VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||||
|
|
||||||
|
EXTERNAL_API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibWJzcC1yZXNvdXJjZUlkIl0sInRpbWVvdXRQZXJpb2QiOjkwMCwidXNlcl9uYW1lIjoic3BhLXVzZXIiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNzI4NTgzNjEyLCJqdGkiOiJjMmU2OTkxOS0yZDU3LTQ5YmYtYTVmZS1lZjVmMzIyNDZkZDIiLCJjbGllbnRfaWQiOiJzcGEtY2xpZW50In0.LE2byaRzsYkchzI4ox9Gz2u6jNmTqZQa-_ttR2PY8e8"
|
||||||
|
EXTERNAL_API_BASE_URL=http://20.11.32.134/rest/td/gasset/asset
|
||||||
|
|
||||||
|
SSO_SECRET="Y6g#Rb@Km=Nz]o^X+fEC~<W/LP%qh;M}9T&v!["
|
||||||
|
SSO_VALIDATION_URL="https://saktitd.army.mil.my/api/sso-validate"
|
||||||
|
SSO_MAX_ATTEMPTS=5
|
||||||
|
SSO_TOKEN_EXPIRATION=300
|
||||||
|
SSO_AUTO_ACTIVATE_USERS=true
|
||||||
|
SSO_LOG_ACTIVITIES=true
|
||||||
|
SSO_CONNECTION_TIMEOUT=10
|
||||||
|
SSO_RESPONSE_TIMEOUT=30
|
||||||
|
SSO_PROXY_URL=http://20.9.91.1:3128
|
||||||
|
SSO_ENABLE_FALLBACK=true
|
||||||
|
SSO_PROXY_ENABLED=true
|
||||||
|
|
||||||
|
SANCTUM_STATEFUL_DOMAINS=20.11.32.50
|
||||||
|
|
||||||
|
BLOCK_API_TOOLS_IN_PRODUCTION=true
|
||||||
|
MIN_USER_AGENT_LENGTH=15
|
||||||
|
API_BLOCKED_MESSAGE="API access is restricted in production environment. Please use the web interface."
|
||||||
|
API_ALLOWED_IPS=
|
||||||
|
API_ALLOWED_USER_AGENTS=
|
||||||
|
|
||||||
|
ENABLE_API_KEY_AUTH=true
|
||||||
|
API_VALID_KEYS=86f58825e5c1e0872d8786092d47e3bd,6461fc5390660c55dc47bdfcd85ed9ae
|
||||||
|
API_LOG_KEY_USAGE=true
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
APP_NAME="SUTERA 3.0"
|
||||||
|
APP_ENV=staging
|
||||||
|
APP_KEY=base64:9vnA0vvAweC8C+/WKyEDOziWp4QPsdKZ/blgDZXIbKQ=
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_TIMEZONE=Asia/Kuala_Lumpur
|
||||||
|
APP_URL=https://sutera.ismailmasseran.com
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
PHP_CLI_SERVER_WORKERS=8
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=error
|
||||||
|
|
||||||
|
DB_CONNECTION=pgsql
|
||||||
|
DB_HOST=pgsql
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_DATABASE=sutera
|
||||||
|
DB_USERNAME=suterauser
|
||||||
|
DB_PASSWORD=Aretus@2025
|
||||||
|
DB_ROOT_PASSWORD=Sutera_Staging@root_2025
|
||||||
|
|
||||||
|
SESSION_DRIVER=redis
|
||||||
|
SESSION_LIFETIME=60
|
||||||
|
SESSION_ENCRYPT=true
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=redis
|
||||||
|
|
||||||
|
CACHE_STORE=redis
|
||||||
|
CACHE_PREFIX=sutera_cache
|
||||||
|
CACHE_DRIVER=redis
|
||||||
|
|
||||||
|
MEMCACHED_HOST=memcached
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=sutera_redis@2025
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=mailpit
|
||||||
|
MAIL_PORT=1025
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
# VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||||
|
# VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||||
|
# VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||||
|
# VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||||
|
# VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||||
|
|
||||||
|
EXTERNAL_API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibWJzcC1yZXNvdXJjZUlkIl0sInRpbWVvdXRQZXJpb2QiOjkwMCwidXNlcl9uYW1lIjoic3BhLXVzZXIiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNzI4NTgzNjEyLCJqdGkiOiJjMmU2OTkxOS0yZDU3LTQ5YmYtYTVmZS1lZjVmMzIyNDZkZDIiLCJjbGllbnRfaWQiOiJzcGEtY2xpZW50In0.LE2byaRzsYkchzI4ox9Gz2u6jNmTqZQa-_ttR2PY8e8"
|
||||||
|
EXTERNAL_API_BASE_URL=http://20.11.32.134/rest/td/gasset/asset
|
||||||
|
|
||||||
|
SSO_SECRET="Y6g#Rb@Km=Nz]o^X+fEC~<W/LP%qh;M}9T&v!["
|
||||||
|
SSO_VALIDATION_URL=http://host.docker.internal:8080/api/sso-validate
|
||||||
|
SSO_MAX_ATTEMPTS=5
|
||||||
|
SSO_TOKEN_EXPIRATION=300
|
||||||
|
SSO_AUTO_ACTIVATE_USERS=true
|
||||||
|
SSO_LOG_ACTIVITIES=true
|
||||||
|
SSO_CONNECTION_TIMEOUT=10
|
||||||
|
SSO_RESPONSE_TIMEOUT=30
|
||||||
|
SSO_PROXY_URL=http://20.9.91.1:3128
|
||||||
|
SSO_ENABLE_FALLBACK=true
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
APP_NAME="SUTERA 3.0 Training"
|
||||||
|
APP_ENV=training
|
||||||
|
APP_KEY=base64:3Rd52ttpV3Yd+yvZWgyheb394GqvsTBlUo108P8l3yI=
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_TIMEZONE=Asia/Kuala_Lumpur
|
||||||
|
APP_URL=http://20.11.32.49
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
PHP_CLI_SERVER_WORKERS=8
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=error
|
||||||
|
|
||||||
|
DB_CONNECTION=pgsql
|
||||||
|
DB_HOST=pgsql
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_DATABASE=sutera
|
||||||
|
DB_USERNAME=suterauser
|
||||||
|
DB_PASSWORD=Aretus@Training_2025
|
||||||
|
DB_ROOT_PASSWORD=Sutera_Training@root_2025
|
||||||
|
|
||||||
|
SESSION_DRIVER=redis
|
||||||
|
SESSION_LIFETIME=60
|
||||||
|
SESSION_ENCRYPT=true
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=20.11.32.49
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=redis
|
||||||
|
|
||||||
|
CACHE_STORE=redis
|
||||||
|
CACHE_PREFIX=sutera_cache
|
||||||
|
CACHE_DRIVER=redis
|
||||||
|
|
||||||
|
MEMCACHED_HOST=memcached
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=sutera_redis@2025
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
|
# MAIL_MAILER=smtp
|
||||||
|
# MAIL_SCHEME=null
|
||||||
|
# MAIL_HOST=mailpit
|
||||||
|
# MAIL_PORT=1025
|
||||||
|
# MAIL_USERNAME=null
|
||||||
|
# MAIL_PASSWORD=null
|
||||||
|
# MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
# MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
# VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||||
|
# VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||||
|
# VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||||
|
# VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||||
|
# VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||||
|
|
||||||
|
EXTERNAL_API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibWJzcC1yZXNvdXJjZUlkIl0sInRpbWVvdXRQZXJpb2QiOjkwMCwidXNlcl9uYW1lIjoic3BhLXVzZXIiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNzI4NTgzNjEyLCJqdGkiOiJjMmU2OTkxOS0yZDU3LTQ5YmYtYTVmZS1lZjVmMzIyNDZkZDIiLCJjbGllbnRfaWQiOiJzcGEtY2xpZW50In0.LE2byaRzsYkchzI4ox9Gz2u6jNmTqZQa-_ttR2PY8e8"
|
||||||
|
EXTERNAL_API_BASE_URL=http://20.11.32.134/rest/td/gasset/asset
|
||||||
|
|
||||||
|
SSO_SECRET="Y6g#Rb@Km=Nz]o^X+fEC~<W/LP%qh;M}9T&v!["
|
||||||
|
SSO_VALIDATION_URL="https://saktitd.army.mil.my/api/sso-validate"
|
||||||
|
SSO_MAX_ATTEMPTS=5
|
||||||
|
SSO_TOKEN_EXPIRATION=300
|
||||||
|
SSO_AUTO_ACTIVATE_USERS=true
|
||||||
|
SSO_LOG_ACTIVITIES=true
|
||||||
|
SSO_CONNECTION_TIMEOUT=10
|
||||||
|
SSO_RESPONSE_TIMEOUT=30
|
||||||
|
SSO_PROXY_URL=http://20.9.91.1:3128
|
||||||
|
SSO_ENABLE_FALLBACK=true
|
||||||
|
SSO_PROXY_ENABLED=true
|
||||||
|
|
||||||
|
SANCTUM_STATEFUL_DOMAINS=20.11.32.49
|
||||||
|
|
||||||
|
BLOCK_API_TOOLS_IN_PRODUCTION=true
|
||||||
|
MIN_USER_AGENT_LENGTH=15
|
||||||
|
API_BLOCKED_MESSAGE="API access is restricted in production environment. Please use the web interface."
|
||||||
|
API_ALLOWED_IPS=
|
||||||
|
API_ALLOWED_USER_AGENTS=
|
||||||
|
|
||||||
|
ENABLE_API_KEY_AUTH=true
|
||||||
|
API_VALID_KEYS=86f58825e5c1e0872d8786092d47e3bd,6461fc5390660c55dc47bdfcd85ed9ae
|
||||||
|
API_LOG_KEY_USAGE=true
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
/.github export-ignore
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
.styleci.yml export-ignore
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.phpunit.cache
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
|
/auth.json
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/vendor
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => 'ActivityLog',
|
||||||
|
];
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\ActivityLog\Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class ActivityLogDatabaseSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// $this->call([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\ActivityLog\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Spatie\Activitylog\Models\Activity;
|
||||||
|
use App\Services\ActivityLogger;
|
||||||
|
|
||||||
|
class ActivityLogController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
// ActivityLogger::logView(new Activity, 'Viewed activity logs');
|
||||||
|
|
||||||
|
$perPage = $request->get('per_page', 10);
|
||||||
|
$perPage = min($perPage, 1000); // Limit max per page to 100
|
||||||
|
$search = trim((string) $request->get('search', ''));
|
||||||
|
|
||||||
|
$query = Activity::latest()->with(['causer']);
|
||||||
|
|
||||||
|
if (! empty($search)) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('description', 'ILIKE', "%{$search}%")
|
||||||
|
->orWhere('subject_type', 'ILIKE', "%{$search}%")
|
||||||
|
->orWhere('event', 'ILIKE', "%{$search}%")
|
||||||
|
->orWhere('causer.name', 'ILIKE', "%{$search}%")
|
||||||
|
->orWhere('causer.email', 'ILIKE', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$activities = $query->paginate($perPage);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $activities->items(),
|
||||||
|
'pagination' => [
|
||||||
|
'current_page' => $activities->currentPage(),
|
||||||
|
'per_page' => $activities->perPage(),
|
||||||
|
'total' => $activities->total(),
|
||||||
|
'last_page' => $activities->lastPage(),
|
||||||
|
'from' => $activities->firstItem(),
|
||||||
|
'to' => $activities->lastItem(),
|
||||||
|
'has_more_pages' => $activities->hasMorePages(),
|
||||||
|
'links' => [
|
||||||
|
'first' => $activities->appends(request()->query())->url(1),
|
||||||
|
'last' => $activities->appends(request()->query())->url($activities->lastPage()),
|
||||||
|
'prev' => $activities->appends(request()->query())->previousPageUrl(),
|
||||||
|
'next' => $activities->appends(request()->query())->nextPageUrl(),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'message' => 'Activity logs retrieved successfully',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('activitylog::create');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the specified resource.
|
||||||
|
*/
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
return view('activitylog::show');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
return view('activitylog::edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, $id) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy($id) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\ActivityLog\Policies;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
use Spatie\Activitylog\Models\Activity;
|
||||||
|
|
||||||
|
class ActivityLogPolicy
|
||||||
|
{
|
||||||
|
use HandlesAuthorization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any models.
|
||||||
|
*/
|
||||||
|
public function viewAny($user): bool
|
||||||
|
{
|
||||||
|
return $user->hasPermissionTo('lihat log aktiviti');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the model.
|
||||||
|
*/
|
||||||
|
public function view($user, ?Activity $activity = null): bool
|
||||||
|
{
|
||||||
|
return $user->hasPermissionTo('lihat log aktiviti');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\ActivityLog\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Nwidart\Modules\Traits\PathNamespace;
|
||||||
|
use RecursiveDirectoryIterator;
|
||||||
|
use RecursiveIteratorIterator;
|
||||||
|
|
||||||
|
class ActivityLogServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
use PathNamespace;
|
||||||
|
|
||||||
|
protected string $name = 'ActivityLog';
|
||||||
|
|
||||||
|
protected string $nameLower = 'activitylog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot the application events.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->registerCommands();
|
||||||
|
$this->registerCommandSchedules();
|
||||||
|
$this->registerTranslations();
|
||||||
|
$this->registerConfig();
|
||||||
|
$this->registerViews();
|
||||||
|
$this->loadMigrationsFrom(module_path($this->name, 'Database/Migrations'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the service provider.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->app->register(EventServiceProvider::class);
|
||||||
|
$this->app->register(RouteServiceProvider::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register commands in the format of Command::class
|
||||||
|
*/
|
||||||
|
protected function registerCommands(): void
|
||||||
|
{
|
||||||
|
// $this->commands([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register command Schedules.
|
||||||
|
*/
|
||||||
|
protected function registerCommandSchedules(): void
|
||||||
|
{
|
||||||
|
// $this->app->booted(function () {
|
||||||
|
// $schedule = $this->app->make(Schedule::class);
|
||||||
|
// $schedule->command('inspire')->hourly();
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register translations.
|
||||||
|
*/
|
||||||
|
public function registerTranslations(): void
|
||||||
|
{
|
||||||
|
$langPath = resource_path('lang/modules/'.$this->nameLower);
|
||||||
|
|
||||||
|
if (is_dir($langPath)) {
|
||||||
|
$this->loadTranslationsFrom($langPath, $this->nameLower);
|
||||||
|
$this->loadJsonTranslationsFrom($langPath);
|
||||||
|
} else {
|
||||||
|
$this->loadTranslationsFrom(module_path($this->name, 'Lang'), $this->nameLower);
|
||||||
|
$this->loadJsonTranslationsFrom(module_path($this->name, 'Lang'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register config.
|
||||||
|
*/
|
||||||
|
protected function registerConfig(): void
|
||||||
|
{
|
||||||
|
$configPath = module_path($this->name, config('modules.paths.generator.config.path'));
|
||||||
|
|
||||||
|
if (is_dir($configPath)) {
|
||||||
|
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath));
|
||||||
|
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||||
|
$config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname());
|
||||||
|
$config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config);
|
||||||
|
$segments = explode('.', $this->nameLower.'.'.$config_key);
|
||||||
|
|
||||||
|
// Remove duplicated adjacent segments
|
||||||
|
$normalized = [];
|
||||||
|
foreach ($segments as $segment) {
|
||||||
|
if (end($normalized) !== $segment) {
|
||||||
|
$normalized[] = $segment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized);
|
||||||
|
|
||||||
|
$this->publishes([$file->getPathname() => config_path($config)], 'config');
|
||||||
|
$this->merge_config_from($file->getPathname(), $key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge config from the given path recursively.
|
||||||
|
*/
|
||||||
|
protected function merge_config_from(string $path, string $key): void
|
||||||
|
{
|
||||||
|
$existing = config($key, []);
|
||||||
|
$module_config = require $path;
|
||||||
|
|
||||||
|
config([$key => array_replace_recursive($existing, $module_config)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register views.
|
||||||
|
*/
|
||||||
|
public function registerViews(): void
|
||||||
|
{
|
||||||
|
$viewPath = resource_path('views/modules/'.$this->nameLower);
|
||||||
|
$sourcePath = module_path($this->name, 'Resources/Views');
|
||||||
|
|
||||||
|
$this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']);
|
||||||
|
|
||||||
|
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower);
|
||||||
|
|
||||||
|
Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the services provided by the provider.
|
||||||
|
*/
|
||||||
|
public function provides(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPublishableViewPaths(): array
|
||||||
|
{
|
||||||
|
$paths = [];
|
||||||
|
foreach (config('view.paths') as $path) {
|
||||||
|
if (is_dir($path.'/modules/'.$this->nameLower)) {
|
||||||
|
$paths[] = $path.'/modules/'.$this->nameLower;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\ActivityLog\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||||
|
|
||||||
|
class EventServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The event handler mappings for the application.
|
||||||
|
*
|
||||||
|
* @var array<string, array<int, string>>
|
||||||
|
*/
|
||||||
|
protected $listen = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if events should be discovered.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected static $shouldDiscoverEvents = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the proper event listeners for email verification.
|
||||||
|
*/
|
||||||
|
protected function configureEmailVerification(): void {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\ActivityLog\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
class RouteServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
protected string $name = 'ActivityLog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before routes are registered.
|
||||||
|
*
|
||||||
|
* Register any model bindings or pattern based filters.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the routes for the application.
|
||||||
|
*/
|
||||||
|
public function map(): void
|
||||||
|
{
|
||||||
|
$this->mapApiRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the "web" routes for the application.
|
||||||
|
*
|
||||||
|
* These routes all receive session state, CSRF protection, etc.
|
||||||
|
*/
|
||||||
|
protected function mapWebRoutes(): void
|
||||||
|
{
|
||||||
|
Route::middleware('web')->group(module_path($this->name, '/Routes/web.php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the "api" routes for the application.
|
||||||
|
*
|
||||||
|
* These routes are typically stateless.
|
||||||
|
*/
|
||||||
|
protected function mapApiRoutes(): void
|
||||||
|
{
|
||||||
|
Route::middleware('api')->name('api.')->group(module_path($this->name, '/Routes/api.php'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Modules\ActivityLog\Http\Controllers\ActivityLogController;
|
||||||
|
|
||||||
|
Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
|
||||||
|
Route::apiResource('activitylogs', ActivityLogController::class)->names('activitylog');
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "nwidart/activitylog",
|
||||||
|
"description": "",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Widart",
|
||||||
|
"email": "n.widart@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [],
|
||||||
|
"aliases": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Modules\\ActivityLog\\": "App",
|
||||||
|
"Modules\\ActivityLog\\Database\\Factories\\": "database/factories/",
|
||||||
|
"Modules\\ActivityLog\\Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Modules\\ActivityLog\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "ActivityLog",
|
||||||
|
"alias": "activitylog",
|
||||||
|
"description": "",
|
||||||
|
"keywords": [],
|
||||||
|
"priority": 0,
|
||||||
|
"providers": [
|
||||||
|
"Modules\\ActivityLog\\Providers\\ActivityLogServiceProvider"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"axios": "^1.1.2",
|
||||||
|
"laravel-vite-plugin": "^0.7.5",
|
||||||
|
"sass": "^1.69.5",
|
||||||
|
"postcss": "^8.3.7",
|
||||||
|
"vite": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Actions\Fortify;
|
||||||
|
|
||||||
|
use App\Traits\NotifiesAdmins;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||||
|
use Modules\Auth\Entities\User;
|
||||||
|
use Modules\Role\Entities\Role;
|
||||||
|
use Modules\User\Notifications\UserActivationNotification;
|
||||||
|
use Modules\Auth\Services\EmailVerificationOtpService;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class CreateNewUser implements CreatesNewUsers
|
||||||
|
{
|
||||||
|
use PasswordValidationRules, NotifiesAdmins;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and create a newly registered user.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
public function create(array $input): User
|
||||||
|
{
|
||||||
|
Validator::make($input, [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique(User::class),
|
||||||
|
],
|
||||||
|
'ic_number' => ['required', 'string', 'max:255'],
|
||||||
|
'password' => ['required', 'string', 'min:8'],
|
||||||
|
])->validate();
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $input['name'],
|
||||||
|
'uuid' => Str::uuid(),
|
||||||
|
'email' => $input['email'],
|
||||||
|
'password' => Hash::make($input['password']),
|
||||||
|
'ic_number' => $input['ic_number'],
|
||||||
|
'status' => 'pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assign role using Spatie permissions
|
||||||
|
$role = Role::where('name', 'Anggota')->first();
|
||||||
|
if ($role) {
|
||||||
|
$user->assignRole($role);
|
||||||
|
}
|
||||||
|
|
||||||
|
app(EmailVerificationOtpService::class)->send($user);
|
||||||
|
|
||||||
|
// Send notification to admins if user requires activation
|
||||||
|
if ($user->status === 'pending') {
|
||||||
|
$this->notifyAdminsForActivation($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify admins about new user requiring activation
|
||||||
|
*/
|
||||||
|
private function notifyAdminsForActivation(User $newUser): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$adminRoles = ['PENTADBIR', 'PS 2 KJC', 'PS 2 ALAT'];
|
||||||
|
|
||||||
|
// Get users with specific roles plus admins (PENTADBIR and DEVELOPER)
|
||||||
|
$adminUsers = $this->getUsersWithRolesAndAdmins($adminRoles);
|
||||||
|
|
||||||
|
$sender = auth()->user() ?? $newUser; // Use current user as sender, or new user if no auth
|
||||||
|
|
||||||
|
foreach ($adminUsers as $admin) {
|
||||||
|
try {
|
||||||
|
$admin->notify(new UserActivationNotification($newUser, $sender));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Failed to send user activation notification: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Failed to notify admins for user activation: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Actions\Fortify;
|
||||||
|
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Laravel\Fortify\Contracts\LoginResponse as ContractsLoginResponse;
|
||||||
|
use Modules\Auth\Services\AuthSessionService;
|
||||||
|
use Modules\Auth\Services\EmailVerificationOtpService;
|
||||||
|
|
||||||
|
class LoginResponse implements ContractsLoginResponse
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected AuthSessionService $authSession,
|
||||||
|
protected EmailVerificationOtpService $otpService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function toResponse($request): JsonResponse
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if (! $user->hasVerifiedEmail()) {
|
||||||
|
$this->otpService->send($user);
|
||||||
|
|
||||||
|
Auth::guard(config('fortify.guard'))->logout();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Sila semak e-mel anda untuk kod pengesahan 6 digit.',
|
||||||
|
'data' => [
|
||||||
|
'email' => $user->email,
|
||||||
|
'requires_email_verification' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->authSession->createAuthResponse(
|
||||||
|
$user,
|
||||||
|
'Login successful'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Actions\Fortify;
|
||||||
|
|
||||||
|
use App\Support\AuthCookie;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Laravel\Fortify\Contracts\LogoutResponse as LogoutResponseContract;
|
||||||
|
|
||||||
|
class LogoutResponse implements LogoutResponseContract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create an HTTP response that represents the object.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*/
|
||||||
|
public function toResponse($request): JsonResponse
|
||||||
|
{
|
||||||
|
if ($request->cookie(AuthCookie::originalUserCookieName())) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Sila tamatkan penyamaran sebelum log keluar.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
// Delete all tokens for this user to ensure clean logout
|
||||||
|
$user->tokens()->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Logout successful',
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
return AuthCookie::clearAuthCookies($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Actions\Fortify;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
trait PasswordValidationRules
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules used to validate passwords.
|
||||||
|
*
|
||||||
|
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
protected function passwordRules(): array
|
||||||
|
{
|
||||||
|
return ['required', 'string', Password::default(), 'confirmed'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Actions\Fortify;
|
||||||
|
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Laravel\Fortify\Contracts\RegisterResponse as RegisterResponseContract;
|
||||||
|
|
||||||
|
class RegisterResponse implements RegisterResponseContract
|
||||||
|
{
|
||||||
|
public function toResponse($request): JsonResponse
|
||||||
|
{
|
||||||
|
$email = $request->user()?->email;
|
||||||
|
|
||||||
|
Auth::guard(config('fortify.guard'))->logout();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Pendaftaran berjaya. Sila semak e-mel anda untuk kod pengesahan 6 digit.',
|
||||||
|
'data' => [
|
||||||
|
'email' => $email,
|
||||||
|
'requires_email_verification' => true,
|
||||||
|
],
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Actions\Fortify;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||||
|
use Modules\Auth\Entities\User;
|
||||||
|
|
||||||
|
class ResetUserPassword implements ResetsUserPasswords
|
||||||
|
{
|
||||||
|
use PasswordValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and reset the user's forgotten password.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
public function reset(User $user, array $input): void
|
||||||
|
{
|
||||||
|
Validator::make($input, [
|
||||||
|
'password' => $this->passwordRules(),
|
||||||
|
])->validate();
|
||||||
|
|
||||||
|
$user->forceFill([
|
||||||
|
'password' => Hash::make($input['password']),
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Actions\Fortify;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||||
|
use Modules\Auth\Entities\User;
|
||||||
|
|
||||||
|
class UpdateUserPassword implements UpdatesUserPasswords
|
||||||
|
{
|
||||||
|
use PasswordValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and update the user's password.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
public function update(User $user, array $input): void
|
||||||
|
{
|
||||||
|
Validator::make($input, [
|
||||||
|
// 'current_password' => ['required', 'string', 'current_password:web'],
|
||||||
|
'password' => $this->passwordRules(),
|
||||||
|
], [
|
||||||
|
// 'current_password.current_password' => __('The provided password does not match your current password.'),
|
||||||
|
])->validateWithBag('updatePassword');
|
||||||
|
|
||||||
|
$user->forceFill([
|
||||||
|
'password' => Hash::make($input['password']),
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Actions\Fortify;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||||
|
use Modules\Auth\Entities\User;
|
||||||
|
|
||||||
|
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validate and update the given user's profile information.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
public function update(User $user, array $input): void
|
||||||
|
{
|
||||||
|
Validator::make($input, [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique('users')->ignore($user->id),
|
||||||
|
],
|
||||||
|
])->validateWithBag('updateProfileInformation');
|
||||||
|
|
||||||
|
if ($input['email'] !== $user->email &&
|
||||||
|
$user instanceof MustVerifyEmail) {
|
||||||
|
$this->updateVerifiedUser($user, $input);
|
||||||
|
} else {
|
||||||
|
$user->forceFill([
|
||||||
|
'name' => $input['name'],
|
||||||
|
'email' => $input['email'],
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the given verified user's profile information.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
protected function updateVerifiedUser(User $user, array $input): void
|
||||||
|
{
|
||||||
|
$user->forceFill([
|
||||||
|
'name' => $input['name'],
|
||||||
|
'email' => $input['email'],
|
||||||
|
'email_verified_at' => null,
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$user->sendEmailVerificationNotification();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => 'Auth',
|
||||||
|
];
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class AuthDatabaseSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// $this->call([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Emails;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class EmailVerificationOtpEmail extends Notification
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected string $otp
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return ['mail'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toMail(object $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
$minutes = (int) config('auth.email_verification.expiry_minutes', 10);
|
||||||
|
|
||||||
|
return (new MailMessage)
|
||||||
|
->subject('Pengesahan E-mel - Kod OTP')
|
||||||
|
->markdown('auth::emails.verification-otp', [
|
||||||
|
'name' => $notifiable->name,
|
||||||
|
'otp' => $this->otp,
|
||||||
|
'minutes' => $minutes,
|
||||||
|
'logoPath' => public_path('images/logo-kopkb.svg'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Entities;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class EmailVerificationOtp extends Model
|
||||||
|
{
|
||||||
|
use HasUuids;
|
||||||
|
|
||||||
|
protected $table = 'email_verification_otps';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'code',
|
||||||
|
'expires_at',
|
||||||
|
'attempts',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'expires_at' => 'datetime',
|
||||||
|
'attempts' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isExpired(): bool
|
||||||
|
{
|
||||||
|
return $this->expires_at->isPast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasExceededMaxAttempts(): bool
|
||||||
|
{
|
||||||
|
return $this->attempts >= (int) config('auth.email_verification.max_attempts', 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Entities;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
use Spatie\Activitylog\LogOptions;
|
||||||
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
|
use Spatie\Permission\Contracts\Permission;
|
||||||
|
use Spatie\Permission\PermissionRegistrar;
|
||||||
|
use Spatie\Permission\Traits\HasPermissions;
|
||||||
|
use Spatie\Permission\Traits\HasRoles;
|
||||||
|
use App\Services\ActiveRoleService;
|
||||||
|
use App\Traits\HasVisibility;
|
||||||
|
use Lab404\Impersonate\Models\Impersonate;
|
||||||
|
use Modules\Role\Entities\Role;
|
||||||
|
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Modules\Auth\Database\Factories\UserFactory> */
|
||||||
|
use HasApiTokens, HasFactory, HasPermissions, HasRoles, HasUuids, Impersonate, LogsActivity, Notifiable, SoftDeletes, HasVisibility;
|
||||||
|
|
||||||
|
protected $table = 'users';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
'ic_number',
|
||||||
|
'position',
|
||||||
|
'phone_number',
|
||||||
|
'image_url',
|
||||||
|
'status',
|
||||||
|
'two_factor_secret',
|
||||||
|
'two_factor_recovery_codes',
|
||||||
|
'two_factor_confirmed_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
|
'two_factor_secret',
|
||||||
|
'two_factor_recovery_codes',
|
||||||
|
'two_factor_confirmed_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
'status' => 'string',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Role used for permission checks in the current session (Sanctum token).
|
||||||
|
*/
|
||||||
|
public function activeRole(): ?Role
|
||||||
|
{
|
||||||
|
return ActiveRoleService::getActiveRole($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasPermissionAction($action): bool
|
||||||
|
{
|
||||||
|
$role = $this->activeRole();
|
||||||
|
|
||||||
|
return $role && $role->permission_actions &&
|
||||||
|
in_array($action, $role->permission_actions, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActivityLogOptions(): LogOptions
|
||||||
|
{
|
||||||
|
return LogOptions::defaults()
|
||||||
|
->logAll()
|
||||||
|
->logOnlyDirty()
|
||||||
|
->dontSubmitEmptyLogs()
|
||||||
|
->setDescriptionForEvent(fn (string $eventName) => "User {$this->name} was {$eventName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override hasPermissionTo to bypass all permission checks for DEVELOPER role
|
||||||
|
*/
|
||||||
|
public function hasPermissionTo($permission, $guardName = null): bool
|
||||||
|
{
|
||||||
|
$activeRole = $this->activeRole();
|
||||||
|
|
||||||
|
if ($activeRole?->name === 'DEVELOPER') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static $isCheckingPermission = false;
|
||||||
|
|
||||||
|
if ($isCheckingPermission) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isCheckingPermission = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$permissionClass = app(PermissionRegistrar::class)->getPermissionClass();
|
||||||
|
|
||||||
|
if (is_string($permission)) {
|
||||||
|
$permission = $permissionClass::findByName($permission, $guardName ?? 'api');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_int($permission)) {
|
||||||
|
$permission = $permissionClass::findById($permission, $guardName ?? 'api');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $permission instanceof Permission) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->permissions->contains('id', $permission->id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $activeRole) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$activeRole->loadMissing('permissions');
|
||||||
|
|
||||||
|
return $activeRole->permissions->contains('id', $permission->id);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
$isCheckingPermission = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to exclude current user unless they have developer role
|
||||||
|
*/
|
||||||
|
public function scopeExcludeCurrentUserUnlessDeveloper($query, $currentUser = null)
|
||||||
|
{
|
||||||
|
$currentUser = $currentUser ?? auth()->user();
|
||||||
|
|
||||||
|
if ($currentUser && ! $currentUser->hasRole('DEVELOPER')) {
|
||||||
|
return $query->where('id', '!=', $currentUser->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to exclude users with DEVELOPER role unless current user is DEVELOPER
|
||||||
|
*/
|
||||||
|
public function scopeExcludeDevelopersUnlessDeveloper($query, $currentUser = null)
|
||||||
|
{
|
||||||
|
$currentUser = $currentUser ?? auth()->user();
|
||||||
|
|
||||||
|
if ($currentUser && ! $currentUser->hasRole('DEVELOPER')) {
|
||||||
|
return $query->whereDoesntHave('roles', function ($q) {
|
||||||
|
$q->where('name', 'DEVELOPER');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user can impersonate another user
|
||||||
|
*/
|
||||||
|
public function canImpersonate(): bool
|
||||||
|
{
|
||||||
|
// Check permission instead of hardcoded roles
|
||||||
|
return $this->hasPermissionTo('menyamar pengguna');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user can be impersonated
|
||||||
|
*/
|
||||||
|
public function canBeImpersonated(): bool
|
||||||
|
{
|
||||||
|
// DEVELOPER cannot be impersonated by anyone
|
||||||
|
if ($this->hasRole('DEVELOPER')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the current user has permission to impersonate
|
||||||
|
$currentUser = auth()->user();
|
||||||
|
if (!$currentUser) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users with permission can impersonate other users (except DEVELOPER)
|
||||||
|
return $currentUser->hasPermissionTo('menyamar pengguna');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasVerifiedEmail(): bool
|
||||||
|
{
|
||||||
|
return $this->email_verified_at !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user can login based on their status
|
||||||
|
*/
|
||||||
|
public function canLogin(): bool
|
||||||
|
{
|
||||||
|
return $this->status === 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether credentials are valid for issuing a session (includes verified pending users).
|
||||||
|
*/
|
||||||
|
public function canAuthenticate(): bool
|
||||||
|
{
|
||||||
|
if ($this->canLogin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->hasVerifiedEmail() && $this->status === 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the login restriction message based on user status
|
||||||
|
*/
|
||||||
|
public function getLoginRestrictionMessage(): ?string
|
||||||
|
{
|
||||||
|
switch ($this->status) {
|
||||||
|
case 'pending':
|
||||||
|
return 'Akaun anda sedang menunggu pengaktifan dari pentadbir sistem. Sila hubungi pentadbir sistem.';
|
||||||
|
case 'inactive':
|
||||||
|
return 'Akaun anda tidak aktif. Sila hubungi pentadbir sistem.';
|
||||||
|
default:
|
||||||
|
return 'Akaun anda tidak dapat mengakses sistem. Sila hubungi pentadbir sistem.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Modules\Auth\Entities\User;
|
||||||
|
use Modules\Auth\Services\AuthSessionService;
|
||||||
|
use Modules\Auth\Services\EmailVerificationOtpService;
|
||||||
|
|
||||||
|
class EmailVerificationController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected EmailVerificationOtpService $otpService,
|
||||||
|
protected AuthSessionService $authSession
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function verify(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'email' => ['required', 'string', 'email'],
|
||||||
|
'otp' => ['required', 'string', 'digits:6'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::where('email', $validated['email'])->first();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Kod OTP tidak sah.',
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->otpService->verify($user, $validated['otp']);
|
||||||
|
|
||||||
|
$user->refresh();
|
||||||
|
|
||||||
|
return $this->authSession->createAuthResponse(
|
||||||
|
$user,
|
||||||
|
'E-mel anda telah berjaya disahkan. Akaun anda sedang menunggu pengaktifan daripada pentadbir sistem.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resend(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'email' => ['required', 'string', 'email'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::where('email', $validated['email'])->first();
|
||||||
|
|
||||||
|
if ($user && ! $user->hasVerifiedEmail()) {
|
||||||
|
$this->otpService->send($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Jika e-mel wujud dan belum disahkan, kod OTP baharu telah dihantar.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\ActiveRoleService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Modules\Auth\Transformers\UserResource;
|
||||||
|
|
||||||
|
class SessionController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Current authenticated user with active role metadata.
|
||||||
|
*/
|
||||||
|
public function currentUser(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Unauthenticated',
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->load(['roles.permissions']);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => new UserResource($user),
|
||||||
|
...ActiveRoleService::sessionMeta($user),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch active role for the current session token.
|
||||||
|
*/
|
||||||
|
public function switchRole(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'role_id' => ['required', 'uuid', 'exists:roles,id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
$user->load(['roles.permissions']);
|
||||||
|
|
||||||
|
$role = ActiveRoleService::switchRole($user, $validated['role_id']);
|
||||||
|
|
||||||
|
if (! $role) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Peranan tidak ditetapkan untuk pengguna ini.',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Peranan aktif dikemas kini.',
|
||||||
|
'data' => new UserResource($user),
|
||||||
|
'active_role' => ActiveRoleService::formatRole($role),
|
||||||
|
'can_switch_role' => $user->roles->count() > 1,
|
||||||
|
'redirect_path' => ActiveRoleService::redirectPathForRole($role),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Laravel\Fortify\Fortify;
|
||||||
|
use Modules\Auth\Actions\Fortify\CreateNewUser;
|
||||||
|
use Modules\Auth\Actions\Fortify\ResetUserPassword;
|
||||||
|
use Modules\Auth\Actions\Fortify\UpdateUserPassword;
|
||||||
|
use Modules\Auth\Actions\Fortify\UpdateUserProfileInformation;
|
||||||
|
use Nwidart\Modules\Traits\PathNamespace;
|
||||||
|
use RecursiveDirectoryIterator;
|
||||||
|
use RecursiveIteratorIterator;
|
||||||
|
|
||||||
|
class AuthServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
use PathNamespace;
|
||||||
|
|
||||||
|
protected string $name = 'Auth';
|
||||||
|
|
||||||
|
protected string $nameLower = 'auth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot the application events.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->registerCommands();
|
||||||
|
$this->registerCommandSchedules();
|
||||||
|
$this->registerTranslations();
|
||||||
|
$this->registerConfig();
|
||||||
|
$this->registerViews();
|
||||||
|
$this->loadMigrationsFrom(module_path($this->name, 'database/migrations'));
|
||||||
|
$this->registerFortify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the service provider.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->app->register(EventServiceProvider::class);
|
||||||
|
$this->app->register(RouteServiceProvider::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register commands in the format of Command::class
|
||||||
|
*/
|
||||||
|
protected function registerCommands(): void
|
||||||
|
{
|
||||||
|
// $this->commands([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register command Schedules.
|
||||||
|
*/
|
||||||
|
protected function registerCommandSchedules(): void
|
||||||
|
{
|
||||||
|
// $this->app->booted(function () {
|
||||||
|
// $schedule = $this->app->make(Schedule::class);
|
||||||
|
// $schedule->command('inspire')->hourly();
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register translations.
|
||||||
|
*/
|
||||||
|
public function registerTranslations(): void
|
||||||
|
{
|
||||||
|
$langPath = resource_path('lang/modules/'.$this->nameLower);
|
||||||
|
|
||||||
|
if (is_dir($langPath)) {
|
||||||
|
$this->loadTranslationsFrom($langPath, $this->nameLower);
|
||||||
|
$this->loadJsonTranslationsFrom($langPath);
|
||||||
|
} else {
|
||||||
|
$this->loadTranslationsFrom(module_path($this->name, 'lang'), $this->nameLower);
|
||||||
|
$this->loadJsonTranslationsFrom(module_path($this->name, 'lang'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register config.
|
||||||
|
*/
|
||||||
|
protected function registerConfig(): void
|
||||||
|
{
|
||||||
|
$configPath = module_path($this->name, config('modules.paths.generator.config.path'));
|
||||||
|
|
||||||
|
if (is_dir($configPath)) {
|
||||||
|
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath));
|
||||||
|
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||||
|
$config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname());
|
||||||
|
$config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config);
|
||||||
|
$segments = explode('.', $this->nameLower.'.'.$config_key);
|
||||||
|
|
||||||
|
// Remove duplicated adjacent segments
|
||||||
|
$normalized = [];
|
||||||
|
foreach ($segments as $segment) {
|
||||||
|
if (end($normalized) !== $segment) {
|
||||||
|
$normalized[] = $segment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized);
|
||||||
|
|
||||||
|
$this->publishes([$file->getPathname() => config_path($config)], 'config');
|
||||||
|
$this->merge_config_from($file->getPathname(), $key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge config from the given path recursively.
|
||||||
|
*/
|
||||||
|
protected function merge_config_from(string $path, string $key): void
|
||||||
|
{
|
||||||
|
$existing = config($key, []);
|
||||||
|
$module_config = require $path;
|
||||||
|
|
||||||
|
config([$key => array_replace_recursive($existing, $module_config)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register views.
|
||||||
|
*/
|
||||||
|
public function registerViews(): void
|
||||||
|
{
|
||||||
|
$viewPath = resource_path('views/modules/'.$this->nameLower);
|
||||||
|
$sourcePath = module_path($this->name, 'resources/views');
|
||||||
|
|
||||||
|
$this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']);
|
||||||
|
|
||||||
|
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower);
|
||||||
|
|
||||||
|
Blade::componentNamespace(config('modules.namespace').'\\'.$this->name.'\\View\\Components', $this->nameLower);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the services provided by the provider.
|
||||||
|
*/
|
||||||
|
public function provides(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPublishableViewPaths(): array
|
||||||
|
{
|
||||||
|
$paths = [];
|
||||||
|
foreach (config('view.paths') as $path) {
|
||||||
|
if (is_dir($path.'/modules/'.$this->nameLower)) {
|
||||||
|
$paths[] = $path.'/modules/'.$this->nameLower;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register Fortify actions.
|
||||||
|
*/
|
||||||
|
protected function registerFortify(): void
|
||||||
|
{
|
||||||
|
Fortify::createUsersUsing(CreateNewUser::class);
|
||||||
|
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
|
||||||
|
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
|
||||||
|
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||||
|
|
||||||
|
class EventServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The event handler mappings for the application.
|
||||||
|
*
|
||||||
|
* @var array<string, array<int, string>>
|
||||||
|
*/
|
||||||
|
protected $listen = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if events should be discovered.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected static $shouldDiscoverEvents = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the proper event listeners for email verification.
|
||||||
|
*/
|
||||||
|
protected function configureEmailVerification(): void {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
class RouteServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
protected $moduleNamespace = 'Modules\Auth\Http\Controllers';
|
||||||
|
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function map()
|
||||||
|
{
|
||||||
|
$this->mapApiRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function mapWebRoutes()
|
||||||
|
{
|
||||||
|
Route::middleware('web')
|
||||||
|
->namespace($this->moduleNamespace)
|
||||||
|
->group(module_path('Auth', '/Routes/web.php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function mapApiRoutes()
|
||||||
|
{
|
||||||
|
Route::middleware('api')
|
||||||
|
->namespace($this->moduleNamespace)
|
||||||
|
->group(module_path('Auth', '/Routes/api.php'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Modules\Auth\Http\Controllers\EmailVerificationController;
|
||||||
|
use Modules\Auth\Http\Controllers\SessionController;
|
||||||
|
use Laravel\Fortify\Http\Controllers\AuthenticatedSessionController;
|
||||||
|
use Laravel\Fortify\Http\Controllers\RegisteredUserController;
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
Route::post('/register', [RegisteredUserController::class, 'store']);
|
||||||
|
Route::post('/login', [AuthenticatedSessionController::class, 'store'])->middleware('block.api.tools');
|
||||||
|
|
||||||
|
Route::post('/verify-email', [EmailVerificationController::class, 'verify'])
|
||||||
|
->middleware('throttle:email-verification');
|
||||||
|
Route::post('/verify-email/resend', [EmailVerificationController::class, 'resend'])
|
||||||
|
->middleware('throttle:email-verification-resend');
|
||||||
|
|
||||||
|
Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
|
||||||
|
// get current user
|
||||||
|
Route::get('/me', [SessionController::class, 'currentUser']);
|
||||||
|
|
||||||
|
// switch active role
|
||||||
|
Route::post('/active-role/switch', [SessionController::class, 'switchRole']);
|
||||||
|
|
||||||
|
// logout
|
||||||
|
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy']);
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Services;
|
||||||
|
|
||||||
|
use App\Services\ActiveRoleService;
|
||||||
|
use App\Support\AuthCookie;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Modules\Auth\Entities\User;
|
||||||
|
use Modules\Auth\Transformers\UserResource;
|
||||||
|
|
||||||
|
class AuthSessionService
|
||||||
|
{
|
||||||
|
public function createAuthResponse(User $user, string $message, int $status = 200): JsonResponse
|
||||||
|
{
|
||||||
|
if (config('app.env') !== 'local') {
|
||||||
|
$user->tokens()->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $user->createToken(
|
||||||
|
name: 'authToken',
|
||||||
|
abilities: ['*'],
|
||||||
|
expiresAt: now()->addMinutes((int) config('auth_cookie.lifetime_minutes', 720))
|
||||||
|
);
|
||||||
|
|
||||||
|
$user->load(['roles.permissions']);
|
||||||
|
|
||||||
|
ActiveRoleService::assignDefaultToToken($user, $token->accessToken);
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'success' => true,
|
||||||
|
'message' => $message,
|
||||||
|
'data' => [
|
||||||
|
'user' => new UserResource($user),
|
||||||
|
'token' => $token->plainTextToken,
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
'expires_at' => $token->accessToken->expires_at,
|
||||||
|
],
|
||||||
|
...ActiveRoleService::sessionMeta($user),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (AuthCookie::shouldExposeTokenInResponse()) {
|
||||||
|
$payload['data']['token'] = $token->plainTextToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = response()->json($payload, $status);
|
||||||
|
|
||||||
|
return AuthCookie::attachAuthToken($response, $token->plainTextToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Modules\Auth\Entities\EmailVerificationOtp;
|
||||||
|
use Modules\Auth\Entities\User;
|
||||||
|
use Modules\Auth\Emails\EmailVerificationOtpEmail;
|
||||||
|
|
||||||
|
class EmailVerificationOtpService
|
||||||
|
{
|
||||||
|
public function send(User $user): void
|
||||||
|
{
|
||||||
|
if ($user->hasVerifiedEmail()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$otp = $this->generateOtp();
|
||||||
|
|
||||||
|
EmailVerificationOtp::query()
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
EmailVerificationOtp::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'code' => Hash::make($otp),
|
||||||
|
'expires_at' => now()->addMinutes($this->expiryMinutes()),
|
||||||
|
'attempts' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user->notify(new EmailVerificationOtpEmail($otp));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verify(User $user, string $otp): void
|
||||||
|
{
|
||||||
|
if ($user->hasVerifiedEmail()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => ['E-mel anda telah disahkan.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$record = EmailVerificationOtp::query()
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->latest()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $record) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'otp' => ['Kod OTP tidak dijumpai. Sila minta kod baharu.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($record->isExpired()) {
|
||||||
|
$record->delete();
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'otp' => ['Kod OTP telah tamat tempoh. Sila minta kod baharu.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($record->hasExceededMaxAttempts()) {
|
||||||
|
$record->delete();
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'otp' => ['Terlalu banyak percubaan. Sila minta kod baharu.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Hash::check($otp, $record->code)) {
|
||||||
|
$record->increment('attempts');
|
||||||
|
|
||||||
|
if ($record->fresh()->hasExceededMaxAttempts()) {
|
||||||
|
$record->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'otp' => ['Kod OTP tidak sah.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->forceFill(['email_verified_at' => now()])->save();
|
||||||
|
|
||||||
|
EmailVerificationOtp::query()
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateOtp(): string
|
||||||
|
{
|
||||||
|
return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function expiryMinutes(): int
|
||||||
|
{
|
||||||
|
return (int) config('auth.email_verification.expiry_minutes', 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Transformers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class AuthResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user' => new UserResource($this->resource),
|
||||||
|
'token' => $this->token ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Transformers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Modules\Position\Transformers\PositionResource;
|
||||||
|
use Modules\Rank\Transformers\RankResource;
|
||||||
|
use Modules\Unit\Transformers\UnitResource;
|
||||||
|
|
||||||
|
class SSOUserResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'uuid' => $this->uuid,
|
||||||
|
'name' => $this->name,
|
||||||
|
'email' => $this->email,
|
||||||
|
'army_number' => $this->army_number,
|
||||||
|
'unit_id' => $this->unit_id,
|
||||||
|
'rank_id' => $this->rank_id,
|
||||||
|
'position_id' => $this->position_id,
|
||||||
|
'phone_number' => $this->phone_number,
|
||||||
|
'image_url' => $this->image_url ? Storage::disk('public')->url($this->image_url) : null,
|
||||||
|
'status' => $this->status,
|
||||||
|
'token' => $this->when(isset($this->token), $this->token),
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
'roles' => $this->whenLoaded('roles', function () {
|
||||||
|
return $this->roles->map(function ($role) {
|
||||||
|
return [
|
||||||
|
'id' => $role->id,
|
||||||
|
'name' => $role->name,
|
||||||
|
'guard_name' => $role->guard_name,
|
||||||
|
'permissions' => $role->permissions ? $role->permissions->map(function ($permission) {
|
||||||
|
return [
|
||||||
|
'id' => $permission->id,
|
||||||
|
'name' => $permission->name,
|
||||||
|
'guard_name' => $permission->guard_name,
|
||||||
|
'route_name' => $permission->route_name ?? null,
|
||||||
|
'created_at' => $permission->created_at,
|
||||||
|
'updated_at' => $permission->updated_at,
|
||||||
|
];
|
||||||
|
}) : [],
|
||||||
|
'created_at' => $role->created_at,
|
||||||
|
'updated_at' => $role->updated_at,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
'unit' => new UnitResource($this->whenLoaded('unit')),
|
||||||
|
'rank' => new RankResource($this->whenLoaded('rank')),
|
||||||
|
'position' => new PositionResource($this->whenLoaded('position')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Auth\Transformers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class UserResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'email' => $this->email,
|
||||||
|
'ic_number' => $this->ic_number,
|
||||||
|
'position' => $this->position,
|
||||||
|
'phone_number' => $this->phone_number,
|
||||||
|
'image_url' => $this->image_url ? Storage::disk('public')->url($this->image_url) : null,
|
||||||
|
'status' => $this->status,
|
||||||
|
'two_factor_secret' => $this->two_factor_secret,
|
||||||
|
'two_factor_recovery_codes' => $this->two_factor_recovery_codes,
|
||||||
|
'two_factor_confirmed_at' => $this->two_factor_confirmed_at,
|
||||||
|
'email_verified_at' => $this->email_verified_at,
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
'deleted_at' => $this->deleted_at,
|
||||||
|
'roles' => $this->whenLoaded('roles', function () {
|
||||||
|
return $this->roles->map(function ($role) {
|
||||||
|
return [
|
||||||
|
'id' => $role->id,
|
||||||
|
'name' => $role->name,
|
||||||
|
'fullname' => $role->fullname ?? null,
|
||||||
|
'context' => $role->context ?? 'member',
|
||||||
|
'guard_name' => $role->guard_name,
|
||||||
|
'permissions' => $this->when($role->relationLoaded('permissions'), function () use ($role) {
|
||||||
|
return $role->permissions->map(function ($permission) {
|
||||||
|
return [
|
||||||
|
'id' => $permission->id,
|
||||||
|
'name' => $permission->name,
|
||||||
|
'guard_name' => $permission->guard_name,
|
||||||
|
'route_name' => $permission->route_name,
|
||||||
|
'created_at' => $permission->created_at,
|
||||||
|
'updated_at' => $permission->updated_at,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
'created_at' => $role->created_at,
|
||||||
|
'updated_at' => $role->updated_at,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "nwidart/auth",
|
||||||
|
"description": "",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Widart",
|
||||||
|
"email": "n.widart@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [],
|
||||||
|
"aliases": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Modules\\Auth\\": "App",
|
||||||
|
"Modules\\Auth\\Database\\Factories\\": "database/factories/",
|
||||||
|
"Modules\\Auth\\Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Modules\\Auth\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "Auth",
|
||||||
|
"alias": "auth",
|
||||||
|
"description": "",
|
||||||
|
"keywords": [],
|
||||||
|
"priority": 0,
|
||||||
|
"providers": [
|
||||||
|
"Modules\\Auth\\Providers\\AuthServiceProvider"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"axios": "^1.1.2",
|
||||||
|
"laravel-vite-plugin": "^0.7.5",
|
||||||
|
"sass": "^1.69.5",
|
||||||
|
"postcss": "^8.3.7",
|
||||||
|
"vite": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
@component('mail::message')
|
||||||
|
@if (! empty($logoPath) && file_exists($logoPath))
|
||||||
|
<div style="text-align: center; margin-bottom: 24px;">
|
||||||
|
<img src="{{ $message->embed($logoPath) }}" alt="{{ config('app.name') }}" style="max-height: 80px; width: auto;">
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
# Pengesahan E-mel
|
||||||
|
|
||||||
|
Assalamualaikum **{{ $name }}**,
|
||||||
|
|
||||||
|
Terima kasih kerana mendaftar. Gunakan kod OTP di bawah untuk mengesahkan alamat e-mel anda.
|
||||||
|
|
||||||
|
@component('mail::panel')
|
||||||
|
<div style="text-align: center; font-size: 28px; font-weight: bold; letter-spacing: 6px;">
|
||||||
|
{{ $otp }}
|
||||||
|
</div>
|
||||||
|
@endcomponent
|
||||||
|
|
||||||
|
Kod ini akan tamat tempoh dalam **{{ $minutes }} minit**.
|
||||||
|
|
||||||
|
Jika anda tidak membuat pendaftaran ini, abaikan e-mel ini.
|
||||||
|
|
||||||
|
Terima kasih,<br>
|
||||||
|
{{ config('app.name') }}
|
||||||
|
@endcomponent
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => 'Dashboard',
|
||||||
|
];
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Dashboard\Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class DashboardDatabaseSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// $this->call([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user