Public website and administration panel for the Bank of Azad Jammu & Kashmir.
- Public site: products, services, rates, financial reports, branch network, news, careers, downloads, loan calculator, contact form
- Admin panel: role-based content management, dashboard, contact-message inbox, menu builder, user/role management
- Public JSON API (v1): branches (with "nearest branch"), regions, districts, contact details, rates, news, careers
- Urdu documentation (right-to-left): docs/ur/README.md
| Backend | Laravel 13, PHP 8.3+ (production runs 8.4) |
| Frontend | React 19 + TypeScript, Inertia.js v3, Tailwind CSS v4, Vite 8 (same stack as the official Laravel React starter kit) |
| Database | MariaDB/MySQL (production), SQLite for tests |
| Auth / permissions | Laravel auth + spatie/laravel-permission v8 (permission middleware) |
| Tests | Pest 4 (about 440 tests) |
- Quick start (local development)
- Configuration (
.env) - Creating the first administrator
- Everyday commands
- Project structure
- Roles and permissions
- Public API v1
- Security
- Testing and quality
- Deployment to production
- Upgrading Laravel
- Troubleshooting
Requirements: PHP 8.3 or newer with mbstring, intl, gd, zip and a database driver (pdo_mysql or pdo_sqlite), Composer 2, Node.js 22+ and npm.
git clone https://github.com/alimarchal/Genexis.git
cd Genexis
composer install
npm ci
cp .env.example .env
php artisan key:generatePick a database in .env.
SQLite (simplest for development):
DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/Genexis/database/database.sqlitetouch database/database.sqliteMariaDB / MySQL:
DB_CONNECTION=mariadb
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=bajk
DB_USERNAME=bajk
DB_PASSWORD=choose-a-strong-passwordThen create the tables, load the starter content and start everything:
php artisan migrate --seed # tables + menus, branches, news, products, roles, permissions ...
php artisan storage:link # makes uploaded files reachable under /storage
php artisan bank:create-admin [email protected] --name="Your Name" # first administrator (asks for a password)
composer run dev # PHP server + queue worker + log viewer + Vite (hot reload)Open http://localhost:8000 for the website and http://localhost:8000/login for the admin panel.
If you see "Unable to locate file in Vite manifest", run
npm run buildonce, or keepcomposer run devrunning.
.env.example documents every key. The important groups:
| Group | Keys | Notes |
|---|---|---|
| Application | APP_ENV, APP_DEBUG, APP_URL, APP_KEY |
Production must use APP_ENV=production, APP_DEBUG=false, an https:// APP_URL. |
| Database | DB_* |
MariaDB in production. |
| Sessions | SESSION_SECURE_COOKIE, SESSION_ENCRYPT |
Set SESSION_SECURE_COOKIE=true on https. |
MAIL_*, CONTACT_EMAIL, CONTACT_FORM_RECIPIENT |
Notification e-mail for the contact form. Messages are always saved in the admin inbox even if mail fails. | |
| Bank details | CONTACT_PHONE, CONTACT_ADDRESS, BANK_BRANCHES_COUNT, SOCIAL_*, *_URL |
Shown in header, footer and contact page. |
| Rates | LOAN_RATE_*, DEPOSIT_RATE_*, LOAN_MIN_*, LOAN_MAX_*, LOAN_TENURE_* |
Loan calculator and rate widgets. |
| Security | TRUSTED_PROXIES, FORCE_HTTPS, CSP_ENABLED, *_RATE_LIMIT |
See Security. |
All bank-specific values are read through config/bank.php. Never call env() outside the config/ folder: after php artisan config:cache it returns null. A test enforces this.
Public registration is disabled. Create administrators from the command line:
php artisan bank:create-admin [email protected] --name="Full Name"The password must be at least 12 characters with letters and numbers. Running the command again for the same e-mail resets the password. Other staff accounts are created in the panel under User Management -> Users.
| Command | What it does |
|---|---|
composer run dev |
Full development stack with hot reload |
php artisan test |
Run the whole test suite |
php artisan test --filter=Name |
Run matching tests |
vendor/bin/pint |
Fix PHP code style |
npm run ci |
Prettier check + ESLint + TypeScript |
npm run format / npm run lint |
Auto-fix JS/TS style |
npm run build |
Production build of the front end |
php artisan bank:check |
Verify that the environment is safe for production |
php artisan bank:create-admin <email> |
Create or reset a super-admin |
php artisan news:populate-excerpts |
Generate missing news excerpts |
app/
Console/Commands/ bank:check, bank:create-admin, ...
Http/Controllers/ public pages, admin CRUD, Admin/ (dashboard, menus, inbox), Api/V1/
Http/Middleware/ SecurityHeaders, Inertia shared data, menus, breadcrumbs
Http/Resources/V1/ JSON shapes of the public API
Models/ Policies/ Services/MenuService.php Support/PublicFile.php
config/bank.php every bank-specific setting (contacts, rates, links, security limits)
routes/
web.php public pages + admin panel
api.php /api/v1 public API
resources/js/
Pages/ one folder per screen (public and admin)
components/ shared UI (SafeImage, header, footer, sidebar ...)
public/images/defaults/ built-in fallback illustrations (shown when an image is missing)
scripts/deploy.sh server-side deployment script
tests/Feature/ Pest tests
docs/ documentation (Urdu admin guide, API reference)
Where to change things:
- Website text and lists (news, careers, carousel, branches...): admin panel, no code needed.
- Navigation menu: admin panel -> System Management -> Admin Menus.
- Contact details, rates, social/footer links:
.env(see above), thenphp artisan config:cacheon the server. - A new admin module: controller + Form Requests + React pages +
Route::resource(...)->guardedBy('x')inroutes/web.php+ permissions inRoleAndPermissionSeeder(and a migration that creates them on existing installs, seedatabase/migrations/*contact_submission_permissions.php).
Access control follows the spatie/laravel-permission best practices: users have roles, roles have permissions, and the application only ever checks permissions (role names are used for nothing but guard rails).
- Every role is editable, including
super-admin. Permissions are namedview|create|edit|delete <module>(for exampleedit carousels) and are granted or taken away per role under User Management -> Roles -> Edit Permissions. There is no hidden bypass: noGate::beforehook, no role-name check in the UI. A permission removed from a role is really gone for everyone holding that role, and can be added back at any time. - Spatie middleware does the enforcing. Admin resources use
->guardedBy('module')(inAppServiceProvider), which maps index/show ->permission:view module, create/store ->permission:create module, edit/update ->permission:edit module, destroy ->permission:delete module. Controllers and one-off routes use the samepermission:alias (seebootstrap/app.phpfor therole,permissionandrole_or_permissionaliases). A user with onlyview carouselscan look but not change anything. - No lock-out. The
super-adminrole must always keepview rolesandedit rolesso roles stay manageable; it cannot be renamed or deleted, and the last super-admin user cannot be removed or demoted. - Seeding never overrides your choices.
RoleAndPermissionSeederonly grants permissions it creates for the first time, so re-running it (or a deploy) never restores a permission an administrator removed. When you add a module, add its permissions to the seeder and to a migration (seedatabase/migrations/*upload_permissions.php) that creates them and grants them tosuper-adminon existing installs. - Staff can only hand out permissions they hold themselves; only a super-admin can create or modify another super-admin.
- Dashboard widgets, sidebar entries and action buttons appear only for permissions the user has (the UI reads
auth.permissions). - Uploads:
view uploadsopens the upload log;upload large filesraises the size ceiling toUPLOAD_MAX_BYTES.
Read-only, no authentication, JSON, cacheable (Cache-Control: public, max-age=300 + ETag), rate limited per IP (default 60 requests/minute; HTTP 429 with Retry-After).
Base URL: https://<your-domain>/api/v1
| Endpoint | Description |
|---|---|
GET /branches |
Active branches. Filters: filter[region_id], filter[district_id], filter[type], filter[is_24_hours], filter[name], filter[address]. Sort: sort=name or sort=-code. per_page (max 100). |
GET /branches/{code} |
One branch by branch code, with phones and e-mails. |
GET /branches/nearby?lat=&lng=&radius_km=&limit= |
Branches around a point, nearest first, with distance_km (default radius 25 km, max 200). |
GET /regions, GET /districts?region_id= |
Geography with active branch counts. |
GET /contact |
Head-office phone, e-mail, address, social links. |
GET /rates |
Loan and deposit rates (% p.a.). |
GET /news, GET /news/{slug} |
Published news (full content on the detail endpoint). |
GET /careers, GET /careers/{id} |
Open vacancies. |
Example:
curl "https://example.bank/api/v1/branches/nearby?lat=34.37&lng=73.47&radius_km=10&limit=3"{ "data": [ { "code": "0001", "name": "Main Branch Muzaffarabad", "distance_km": 3.1,
"phones": ["05822-921181"], "facilities": ["Safe Deposit Boxes"], "...": "..." } ] }Full reference: docs/api.md. The older GET /api/search?q= endpoint (site search) is unchanged.
Financial statements, annual reports, highlights, downloads, schedules of charges and vacancy documents can be several GB. A normal form post cannot carry that (PHP, the web server and proxies all cap request bodies), so these forms use a resumable chunked uploader:
- The browser cuts the file into 4 MB parts, sends them one by one and shows a progress bar with amount, speed and time left. It can pause, resume, cancel, retries automatically after network errors and can continue after a page refresh.
- Everybody who may manage a module can upload large files for it. Anyone with the module's create/edit permission (financial reports, annual reports, highlights, downloads) may upload up to 10 GB (
UPLOAD_LARGE_MAX_BYTES); schedules of charges and vacancy documents up to 500 MB (UPLOAD_DOCUMENT_MAX_BYTES). Users without the permission cannot upload at all, and anyone holdingupload large files(super-admin by default) may go up toUPLOAD_MAX_BYTES. No PHP setting needs to be changed: because the file travels in 4 MB parts, PHP'supload_max_filesize/post_max_sizenever apply. - The server keeps a record of every upload (who, which file, size, bytes received, IP, status, SHA-256 of the finished file). Users with
view uploadssee it under System Management -> Upload Log; running uploads refresh live. - A finished file is checked by its real content (not the browser's claim) against the allowed types, moved into
storage/app/public, and only attached to a record when the form is saved. A replaced file is deleted only after the new one is saved. - Abandoned uploads and finished files that were never attached are removed after 24 hours by
php artisan uploads:prune(hourly through the scheduler, see below). - Downloads use HTTP Range, so browsers can resume large downloads.
Server requirements:
- The web server must accept request bodies of at least the chunk size, e.g. nginx
client_max_body_size 16m;(this is not the file size limit). PHP'supload_max_filesize/post_max_sizedo not need raising. - Free disk space of at least the file size plus the
UPLOAD_RESERVE_BYTESreserve (php artisan bank:checkwarns when the disk is too small). The upload is assembled instorage/app/private/uploads/tmpand then moved, so the file exists once when finished. - The Laravel scheduler must run for automatic clean-up:
* * * * * cd /var/www/bajk && php artisan schedule:run >> /dev/null 2>&1. - If a CDN such as Cloudflare sits in front, the 4 MB chunks stay far below its 100 MB request limit.
Built in:
- Authorization: per-action permissions, privilege-escalation guards, no public registration.
- Browser protections: nonce-based Content-Security-Policy,
X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy, HSTS on https (app/Http/Middleware/SecurityHeaders.php). SetCSP_ENABLED=falseonly for debugging. - Rate limits per IP: pages 600/min, API 60/min, downloads 30/min, contact form 5 per 10 min (
WEB_RATE_LIMIT,API_RATE_LIMIT,DOWNLOAD_RATE_LIMIT,CONTACT_RATE_LIMIT). Behind a reverse proxy (Cloudflare, nginx, cPanel) setTRUSTED_PROXIES(*or a comma-separated list of proxy IPs); otherwise every visitor looks like the proxy and shares one limit. - Input safety: Form Requests everywhere, menu links reject
javascript:/data:URLs, file downloads only serve active records and never accept paths from the visitor. - Privacy: contact-form details are not written to log files.
- Deployment guard:
php artisan bank:checkstops a deployment ifAPP_DEBUG=truein production.
Operational checklist for the server:
APP_ENV=production,APP_DEBUG=false,APP_URL=https://...,SESSION_SECURE_COOKIE=true,LOG_LEVEL=error- A real mail server in
MAIL_*(notmailpit) - Database user with access only to the application database
- The web server's document root is the
public/folder (never the project root);.envis not web-accessible - Regular database and
storage/app/publicbackups - Keep dependencies current (Dependabot pull requests,
composer audit)
Report security problems privately to the repository owner.
php artisan test # PHP tests (SQLite in memory)
vendor/bin/pint --test # style check
npm run ci # prettier + eslint + tscThe tests are behavioural: for example they verify that a view-only user cannot delete, that inactive documents cannot be downloaded, that hidden news is not shown, that distances in the nearby-branches API are correct and that every route('...') used by the React code exists on the server. GitHub Actions runs everything on PHP 8.3 and 8.4 for every pull request (.github/workflows/ci.yml).
Production assumptions: Linux, PHP 8.3+ (8.4 recommended), MariaDB 10.6+/11, a web server whose document root is public/, Composer on the server, SSH access.
git clone https://github.com/alimarchal/Genexis.git /var/www/bajk
cd /var/www/bajk
cp .env.example .env # then edit: APP_ENV, APP_DEBUG=false, APP_URL, DB_*, MAIL_*, TRUSTED_PROXIES ...
composer install --no-dev --optimize-autoloader
php artisan key:generate
php artisan migrate --force --seed
php artisan storage:link
php artisan bank:create-admin [email protected]
php artisan bank:check # must report no errors
php artisan config:cache && php artisan route:cache && php artisan view:cache && php artisan event:cacheMake storage/ and bootstrap/cache/ writable by the web-server user. Build assets once (npm ci && npm run build) or upload public/build/ from CI.
An existing production database that already matches the migrations needs only php artisan migrate --force (new migrations add permissions only).
.github/workflows/deploy.yml runs after CI passes on main. Add these repository secrets (Settings -> Secrets and variables -> Actions); without them the workflow skips itself:
| Secret | Example |
|---|---|
DEPLOY_HOST |
server.example.com |
DEPLOY_USER |
deploy |
DEPLOY_PATH |
/var/www/bajk |
DEPLOY_SSH_KEY |
private key of a deploy-only user |
DEPLOY_PORT |
22 (optional) |
On each push to main the workflow builds the assets, uploads public/build, then runs scripts/deploy.sh on the server: pull (fast-forward only), composer install --no-dev, php artisan bank:check, maintenance mode, migrate --force, caches, queue restart, back online. If bank:check finds an error nothing on the live site is touched.
Manual deployment is the same command on the server: bash scripts/deploy.sh.
git log --oneline -5 # choose the previous good commit
git checkout <commit> && composer install --no-dev -o
php artisan config:cache && php artisan route:cache && php artisan view:cacheMigrations are additive; restore the database backup only if a migration itself is at fault.
The code follows framework conventions (no framework-internal hacks, bootstrap/app.php configuration, Form Requests, resources, config-only env()), so major upgrades should be small:
- Create a branch and read the official upgrade guide for the target version.
- Raise
laravel/frameworkincomposer.json, thencomposer update -W. Dependabot already proposes minor updates weekly. - Packages that usually need a matching bump:
inertiajs/inertia-laravel,spatie/*,pestphp/*,laravel/boost,laravel/tinker. - Run
php artisan test,npm run ciand open the main pages (CI does the first two automatically). composer.jsonpinsconfig.platform.phpto8.3.0so the lock file installs on 8.3 and 8.4; raise it when you drop 8.3.
| Symptom | Cause / fix |
|---|---|
| White page / "Unable to locate file in Vite manifest" | Assets not built: npm run build |
| Images missing (default illustration shown) | File not uploaded or php artisan storage:link not run |
Values from .env ignored after editing |
Config is cached: php artisan config:cache (or config:clear in development) |
| Contact form works but no e-mail arrives | Check MAIL_*; messages are still in Admin -> Contact Messages |
| Everyone gets "Too many requests" | Behind a proxy without TRUSTED_PROXIES; set it |
| 403 in the admin panel | The user's role lacks that module's permission (User Management -> Roles) |
bank:check fails with APP_DEBUG |
Set APP_DEBUG=false in the production .env |
| Session cookie error "name cannot be empty" | Remove an empty SESSION_COOKIE= line from .env |
Proprietary - Bank of Azad Jammu & Kashmir. All rights reserved.