initial commit
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# Example environment file for WebGIS.
|
||||
# Copy this file to `.env`, then fill in real values. Do not commit `.env`.
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASS=change_me
|
||||
DB_NAME=webgis
|
||||
|
||||
# Mail
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your_gmail_address@gmail.com
|
||||
SMTP_PASS=your_gmail_app_password
|
||||
SMTP_ENCRYPTION=tls
|
||||
MAIL_FROM=your_gmail_address@gmail.com
|
||||
MAIL_FROM_NAME=WebGIS
|
||||
|
||||
# Auth secrets
|
||||
# Generate secrets with: php -r "echo bin2hex(random_bytes(32)).PHP_EOL;"
|
||||
API_KEY=change_me_to_a_random_hex_string
|
||||
JWT_SECRET=change_me_to_a_random_hex_string
|
||||
INTERNAL_AUTH_KEY=change_me_to_a_random_hex_string
|
||||
BASIC_API_USER=webgis-api
|
||||
BASIC_API_PASS=change_me
|
||||
|
||||
# Optional local debug helper for verification codes.
|
||||
DEBUG_MODE=0
|
||||
@@ -0,0 +1,9 @@
|
||||
/.env
|
||||
/.env.local
|
||||
/*.env
|
||||
/uploads/
|
||||
/src/uploads/
|
||||
# ignore common editor temp files
|
||||
*.swp
|
||||
*.swo
|
||||
.vscode/
|
||||
@@ -0,0 +1,141 @@
|
||||
# WebGIS
|
||||
|
||||
Aplikasi peta lokasi bantuan dan tempat ibadah berbasis PHP, MySQL, dan Leaflet.
|
||||
|
||||
## Gambaran Singkat
|
||||
|
||||
Guest bisa melihat peta dan daftar data. Pengelola mendaftar lewat formulir yang langsung memverifikasi email, lalu menunggu persetujuan admin. Setelah akun aktif, pengelola bisa menambah, mengubah, dan menghapus data miliknya sendiri. Admin bisa menyetujui atau menolak akun pengelola yang menunggu aktivasi.
|
||||
|
||||
## Yang Dibutuhkan
|
||||
|
||||
- PHP 8.1 atau lebih baru
|
||||
- MySQL atau MariaDB
|
||||
- Composer
|
||||
- Browser modern seperti Chrome, Edge, atau Firefox
|
||||
- Koneksi internet untuk Leaflet, tile peta, dan email SMTP
|
||||
|
||||
## Langkah Instalasi
|
||||
|
||||
Ikuti urutan ini dari awal jika Anda belum pernah menjalankan proyek ini.
|
||||
|
||||
1. Salin folder proyek ke komputer Anda.
|
||||
2. Buka terminal di folder proyek.
|
||||
3. Pasang dependensi PHP:
|
||||
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
4. Buat file konfigurasi lingkungan:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
5. Isi file `.env` dengan kredensial database, SMTP, dan secret aplikasi.
|
||||
6. Buat database MySQL, misalnya bernama `webgis`.
|
||||
7. Import skema database:
|
||||
|
||||
```bash
|
||||
mysql -u root -p webgis < sql/migrations/001_init_schema.sql
|
||||
```
|
||||
|
||||
8. Jalankan server PHP bawaan untuk uji lokal:
|
||||
|
||||
```bash
|
||||
php -S localhost:8000 -t .
|
||||
```
|
||||
|
||||
9. Buka aplikasi di browser:
|
||||
|
||||
```text
|
||||
http://localhost:8000/index.html
|
||||
```
|
||||
|
||||
Jika Anda memakai Apache atau Nginx, arahkan document root ke folder proyek ini dan sesuaikan URL aksesnya.
|
||||
|
||||
## Isi File `.env`
|
||||
|
||||
Contoh nilai yang perlu diisi:
|
||||
|
||||
- `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME`
|
||||
- `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_ENCRYPTION`
|
||||
- `MAIL_FROM`, `MAIL_FROM_NAME`
|
||||
- `API_KEY`, `JWT_SECRET`, `INTERNAL_AUTH_KEY`
|
||||
- `BASIC_API_USER`, `BASIC_API_PASS`
|
||||
- `DEBUG_MODE=0` untuk produksi atau `1` jika ingin menampilkan kode verifikasi saat pengembangan lokal
|
||||
|
||||
## Cara Pakai
|
||||
|
||||
### 1. Buka peta
|
||||
|
||||
Setelah aplikasi terbuka, tampilan awal adalah peta. Guest tetap bisa melihat marker dan daftar data.
|
||||
|
||||
### 2. Daftar pengelola
|
||||
|
||||
Klik tombol masuk/daftar, lalu pilih daftar pengelola. Isi semua field yang diminta, kirim kode verifikasi ke email, masukkan kode 6 digit, lalu klik daftar.
|
||||
|
||||
### 3. Login
|
||||
|
||||
Gunakan email dan password yang sudah terdaftar. Setelah login, bar atas akan menampilkan nama pengguna.
|
||||
|
||||
### 4. Kelola data
|
||||
|
||||
Pengelola yang sudah aktif bisa menambah data baru dari peta, mengubah data miliknya sendiri, dan mengunggah foto bukti jika diperlukan. Admin bisa mengelola seluruh data.
|
||||
|
||||
### 5. Lihat daftar data
|
||||
|
||||
Klik tombol `Daftar Data` untuk melihat data dalam bentuk tabel. Guest hanya melihat data yang aman untuk publik.
|
||||
|
||||
## Akun Admin Awal
|
||||
|
||||
Seed database menyediakan akun admin awal:
|
||||
|
||||
- Email: `admin@example.com`
|
||||
- Password: `Admin123!`
|
||||
|
||||
Segera ganti kredensial ini jika Anda menjalankan aplikasi di lingkungan yang tidak pribadi.
|
||||
|
||||
## Endpoint Utama
|
||||
|
||||
- `src/api/auth/register.php` - daftar pengelola
|
||||
- `src/api/auth/send_verification_code.php` - kirim kode verifikasi email
|
||||
- `src/api/auth/login.php` - login
|
||||
- `src/api/auth/logout.php` - logout
|
||||
- `src/api/auth/me.php` - status pengguna aktif
|
||||
- `src/api/get_lokasi.php` - ambil data lokasi
|
||||
- `src/api/tambah_lokasi.php` - tambah lokasi baru
|
||||
- `src/api/update_lokasi.php` - ubah lokasi
|
||||
- `src/api/hapus_lokasi.php` - hapus lokasi
|
||||
- `src/api/upload_photo.php` - unggah foto lokasi
|
||||
- `src/api/admin/list_pending_managers.php` - daftar akun pengelola menunggu persetujuan
|
||||
- `src/api/admin/approve_user.php` - setujui akun
|
||||
- `src/api/admin/reject_user.php` - tolak akun
|
||||
|
||||
## File Penting
|
||||
|
||||
- [index.html](index.html) - frontend utama
|
||||
- [src/config/koneksi.php](src/config/koneksi.php) - koneksi database
|
||||
- [src/config/config.php](src/config/config.php) - secret dan autentikasi
|
||||
- [sql/migrations/001_init_schema.sql](sql/migrations/001_init_schema.sql) - skema database utama
|
||||
- [webgis.sql](webgis.sql) - dump SQL yang sejalan dengan skema utama
|
||||
- [.env.example](.env.example) - contoh konfigurasi environment
|
||||
|
||||
## Perawatan Berkala
|
||||
|
||||
Jika Anda ingin menandai bantuan yang sudah lewat masa berlakunya, jalankan:
|
||||
|
||||
```bash
|
||||
php scripts/expire_assistance.php
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Jika database gagal terhubung, cek isi `.env` dan pastikan MySQL berjalan.
|
||||
- Jika email tidak terkirim, pastikan kredensial SMTP benar dan memakai app password, bukan password akun biasa.
|
||||
- Jika peta kosong, cek koneksi internet dan pastikan endpoint `get_lokasi.php` mengembalikan JSON.
|
||||
- Jika login berhasil tetapi data tidak muncul, buka DevTools browser dan cek respons API serta status token JWT.
|
||||
|
||||
## Catatan Pengembangan
|
||||
|
||||
Arsitektur aplikasi ini sengaja dibuat sederhana agar mudah dipelihara: frontend vanilla JavaScript, backend PHP, dan database MySQL. Bila Anda menambah kolom database baru, perbarui file migrasi, dump SQL, dan dokumentasi dalam satu perubahan agar semuanya tetap sinkron.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"require": {
|
||||
"phpmailer/phpmailer": "^7.1",
|
||||
"vlucas/phpdotenv": "^5.6"
|
||||
}
|
||||
}
|
||||
Generated
+574
@@ -0,0 +1,574 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "44f3cb809a0f474d00b34192bc993c72",
|
||||
"packages": [
|
||||
{
|
||||
"name": "graham-campbell/result-type",
|
||||
"version": "v1.1.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/GrahamCampbell/Result-Type.git",
|
||||
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b",
|
||||
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"phpoption/phpoption": "^1.9.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GrahamCampbell\\ResultType\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
}
|
||||
],
|
||||
"description": "An Implementation Of The Result Type",
|
||||
"keywords": [
|
||||
"Graham Campbell",
|
||||
"GrahamCampbell",
|
||||
"Result Type",
|
||||
"Result-Type",
|
||||
"result"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
|
||||
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/GrahamCampbell",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-27T19:43:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v7.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-ctype": "*",
|
||||
"ext-filter": "*",
|
||||
"ext-hash": "*",
|
||||
"php": ">=5.5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
|
||||
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
||||
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
||||
"phpcompatibility/php-compatibility": "^10.0.0@dev",
|
||||
"squizlabs/php_codesniffer": "^3.13.5",
|
||||
"yoast/phpunit-polyfills": "^1.0.4"
|
||||
},
|
||||
"suggest": {
|
||||
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
|
||||
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
|
||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
|
||||
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
|
||||
"psr/log": "For optional PSR-3 debug logging",
|
||||
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
|
||||
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPMailer\\PHPMailer\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marcus Bointon",
|
||||
"email": "phpmailer@synchromedia.co.uk"
|
||||
},
|
||||
{
|
||||
"name": "Jim Jagielski",
|
||||
"email": "jimjag@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Andy Prevost",
|
||||
"email": "codeworxtech@users.sourceforge.net"
|
||||
},
|
||||
{
|
||||
"name": "Brent R. Matzelle"
|
||||
}
|
||||
],
|
||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/Synchro",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-18T08:06:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"version": "1.9.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/schmittjoh/php-option.git",
|
||||
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be",
|
||||
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
"phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"bamarni-bin": {
|
||||
"bin-links": true,
|
||||
"forward-command": false
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "1.9-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpOption\\": "src/PhpOption/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Johannes M. Schmitt",
|
||||
"email": "schmittjoh@gmail.com",
|
||||
"homepage": "https://github.com/schmittjoh"
|
||||
},
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
}
|
||||
],
|
||||
"description": "Option Type for PHP",
|
||||
"keywords": [
|
||||
"language",
|
||||
"option",
|
||||
"php",
|
||||
"type"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/schmittjoh/php-option/issues",
|
||||
"source": "https://github.com/schmittjoh/php-option/tree/1.9.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/GrahamCampbell",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-27T19:41:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "141046a8f9477948ff284fa65be2095baafb94f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
|
||||
"reference": "141046a8f9477948ff284fa65be2095baafb94f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-ctype": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Ctype\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gert de Pagter",
|
||||
"email": "BackEndTea@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for ctype functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"ctype",
|
||||
"polyfill",
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.38.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92",
|
||||
"reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-iconv": "*",
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for the Mbstring extension",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"mbstring",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-26T12:51:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
|
||||
"reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Php80\\": ""
|
||||
},
|
||||
"classmap": [
|
||||
"Resources/stubs"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ion Bazan",
|
||||
"email": "ion.bazan@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "vlucas/phpdotenv",
|
||||
"version": "v5.6.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vlucas/phpdotenv.git",
|
||||
"reference": "955e7815d677a3eaa7075231212f2110983adecc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc",
|
||||
"reference": "955e7815d677a3eaa7075231212f2110983adecc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-pcre": "*",
|
||||
"graham-campbell/result-type": "^1.1.4",
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"phpoption/phpoption": "^1.9.5",
|
||||
"symfony/polyfill-ctype": "^1.26",
|
||||
"symfony/polyfill-mbstring": "^1.26",
|
||||
"symfony/polyfill-php80": "^1.26"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
"ext-filter": "*",
|
||||
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-filter": "Required to use the boolean validator."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"bamarni-bin": {
|
||||
"bin-links": true,
|
||||
"forward-command": false
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "5.6-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dotenv\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
},
|
||||
{
|
||||
"name": "Vance Lucas",
|
||||
"email": "vance@vancelucas.com",
|
||||
"homepage": "https://github.com/vlucas"
|
||||
}
|
||||
],
|
||||
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
|
||||
"keywords": [
|
||||
"dotenv",
|
||||
"env",
|
||||
"environment"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/vlucas/phpdotenv/issues",
|
||||
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/GrahamCampbell",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-27T19:49:13+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.2.0"
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
# ERD Summary - WebGIS
|
||||
|
||||
Dokumen ini merangkum skema database yang dipakai aplikasi pada `sql/migrations/001_init_schema.sql` dan `webgis.sql`.
|
||||
|
||||
## Tabel Utama
|
||||
|
||||
### `users`
|
||||
|
||||
Menyimpan akun pengguna aplikasi.
|
||||
|
||||
Kolom penting:
|
||||
- `email`
|
||||
- `password_hash`
|
||||
- `name`
|
||||
- `organization`
|
||||
- `org_address`
|
||||
- `org_phone`
|
||||
- `org_proof_path`
|
||||
- `role` dengan nilai `manager` atau `admin`
|
||||
- `status` dengan nilai `pending`, `active`, `rejected`, atau `pending_verification`
|
||||
- `email_verified`
|
||||
- field verifikasi email seperti `email_verification_token_hash`, `email_verification_expires`, dan timestamp terkait
|
||||
- `approved_by` dan `approved_at`
|
||||
|
||||
### `lokasi`
|
||||
|
||||
Menyimpan seluruh marker yang tampil di peta.
|
||||
|
||||
Kolom penting:
|
||||
- `nama`
|
||||
- `no_telp`
|
||||
- `latitude` dan `longitude`
|
||||
- `alamat`
|
||||
- `jenis`
|
||||
- field khusus rumah seperti `nama_kk`, `rumah_status`, `members`, `monthly_income`
|
||||
- field bantuan seperti `assisted`, `last_assisted_at`, `assistance_notes`, `sumber_bantuan`, `bentuk_bantuan`
|
||||
- `photo_path`
|
||||
- `created_by`
|
||||
|
||||
### `bantuan_detail`
|
||||
|
||||
Menyimpan detail histori bantuan per lokasi.
|
||||
|
||||
Kolom penting:
|
||||
- `lokasi_id`
|
||||
- `tanggal_bantuan`
|
||||
- `pemberi_bantuan`
|
||||
- `catatan`
|
||||
- `pemberi_lokasi_id`
|
||||
- `jumlah`
|
||||
|
||||
### `migrations`
|
||||
|
||||
Menyimpan catatan migration yang sudah dijalankan.
|
||||
|
||||
## Relasi
|
||||
|
||||
- `users.approved_by` mengarah ke `users.id`
|
||||
- `lokasi.created_by` mengarah ke `users.id`
|
||||
- `lokasi.sumber_bantuan` mengarah ke `lokasi.id`
|
||||
- `bantuan_detail.lokasi_id` mengarah ke `lokasi.id`
|
||||
- `bantuan_detail.pemberi_lokasi_id` mengarah ke `lokasi.id`
|
||||
|
||||
## Alur Data
|
||||
|
||||
1. Guest melihat data publik di peta dan tabel.
|
||||
2. Pengelola mendaftar, menerima kode verifikasi email, lalu akun dibuat dalam status menunggu persetujuan admin.
|
||||
3. Admin menyetujui akun pengelola.
|
||||
4. Pengelola yang disetujui dapat menambah dan mengubah data miliknya.
|
||||
5. Admin bisa mengelola seluruh data.
|
||||
|
||||
## Catatan Implementasi
|
||||
|
||||
- Database yang dipakai adalah MySQL/MariaDB.
|
||||
- Semua relasi foreign key dibuat agar data tetap konsisten.
|
||||
- Kolom bantuan dan verifikasi email disimpan langsung di tabel utama supaya alurnya sederhana untuk pengembangan berikutnya.
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
window.webgisOpenApiSpec = {
|
||||
openapi: '3.0.3',
|
||||
info: {
|
||||
title: 'WebGIS API',
|
||||
version: '1.0.0',
|
||||
description: 'API for public map data, manager registrations, and admin approvals.'
|
||||
},
|
||||
servers: [{ url: '.' }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT'
|
||||
}
|
||||
}
|
||||
},
|
||||
paths: {
|
||||
'/src/api/auth/register.php': {
|
||||
post: {
|
||||
summary: 'Register manager account',
|
||||
description: 'Public registration endpoint for manager accounts.',
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['email', 'password'],
|
||||
properties: {
|
||||
email: { type: 'string', format: 'email' },
|
||||
password: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
organization: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: { '200': { description: 'Registration successful, pending approval' } }
|
||||
}
|
||||
},
|
||||
'/src/api/auth/login.php': {
|
||||
post: {
|
||||
summary: 'Login',
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['email', 'password'],
|
||||
properties: {
|
||||
email: { type: 'string', format: 'email' },
|
||||
password: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: { '200': { description: 'Login successful' } }
|
||||
}
|
||||
},
|
||||
'/src/api/auth/logout.php': {
|
||||
post: {
|
||||
summary: 'Logout',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: { '200': { description: 'Logout successful' } }
|
||||
}
|
||||
},
|
||||
'/src/api/auth/me.php': {
|
||||
get: {
|
||||
summary: 'Current user session',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: { '200': { description: 'Current auth state' } }
|
||||
}
|
||||
},
|
||||
'/src/api/get_lokasi.php': {
|
||||
get: {
|
||||
summary: 'List locations',
|
||||
description: 'Public read-only endpoint.',
|
||||
responses: { '200': { description: 'Location list' } }
|
||||
}
|
||||
},
|
||||
'/src/api/tambah_lokasi.php': {
|
||||
post: {
|
||||
summary: 'Create location',
|
||||
description: 'Bearer JWT required. Managers can create records; admins can create all.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nama: { type: 'string' },
|
||||
jenis: { type: 'string' },
|
||||
alamat: { type: 'string' },
|
||||
latitude: { type: 'number' },
|
||||
longitude: { type: 'number' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: { '200': { description: 'Created' } }
|
||||
}
|
||||
},
|
||||
'/src/api/update_lokasi.php': {
|
||||
post: {
|
||||
summary: 'Update location',
|
||||
description: 'Bearer JWT required. Managers can edit only their own lokasi; admins can edit all.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: { '200': { description: 'Updated' } }
|
||||
}
|
||||
},
|
||||
'/src/api/hapus_lokasi.php': {
|
||||
post: {
|
||||
summary: 'Delete location',
|
||||
description: 'Bearer JWT required. Managers can delete only their own lokasi; admins can delete all.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: { '200': { description: 'Deleted' } }
|
||||
}
|
||||
},
|
||||
'/src/api/upload_photo.php': {
|
||||
post: {
|
||||
summary: 'Upload location photo',
|
||||
description: 'Bearer JWT required. Managers can upload only for their own lokasi.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: { '200': { description: 'Uploaded' } }
|
||||
}
|
||||
},
|
||||
'/src/api/admin/list_pending_managers.php': {
|
||||
get: {
|
||||
summary: 'List pending manager registrations',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: { '200': { description: 'Pending list' } }
|
||||
}
|
||||
},
|
||||
'/src/api/admin/approve_user.php': {
|
||||
post: {
|
||||
summary: 'Approve pending user',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: { '200': { description: 'Approved' } }
|
||||
}
|
||||
},
|
||||
'/src/api/admin/reject_user.php': {
|
||||
post: {
|
||||
summary: 'Reject pending user',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: { '200': { description: 'Rejected' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,208 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: WebGIS API
|
||||
version: 1.0.0
|
||||
description: API untuk autentikasi pengelola, data lokasi, unggah foto, dan persetujuan admin.
|
||||
servers:
|
||||
- url: .
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
basicAuth:
|
||||
type: http
|
||||
scheme: basic
|
||||
schemas:
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
- name
|
||||
- organization
|
||||
- org_address
|
||||
- org_phone
|
||||
- verify_code
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
password:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
organization:
|
||||
type: string
|
||||
org_address:
|
||||
type: string
|
||||
org_phone:
|
||||
type: string
|
||||
verify_code:
|
||||
type: string
|
||||
pattern: '^[0-9]{6}$'
|
||||
LoginRequest:
|
||||
type: object
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
password:
|
||||
type: string
|
||||
LocationRequest:
|
||||
type: object
|
||||
properties:
|
||||
nama:
|
||||
type: string
|
||||
jenis:
|
||||
type: string
|
||||
no_telp:
|
||||
type: string
|
||||
alamat:
|
||||
type: string
|
||||
latitude:
|
||||
type: number
|
||||
longitude:
|
||||
type: number
|
||||
assisted:
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
paths:
|
||||
/src/api/auth/register.php:
|
||||
post:
|
||||
summary: Daftar pengelola baru
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RegisterRequest'
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RegisterRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Pendaftaran berhasil dan akun menunggu persetujuan admin
|
||||
/src/api/auth/send_verification_code.php:
|
||||
post:
|
||||
summary: Kirim kode verifikasi email
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [email]
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
responses:
|
||||
'200':
|
||||
description: Kode verifikasi terkirim
|
||||
/src/api/auth/login.php:
|
||||
post:
|
||||
summary: Login pengguna
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Login berhasil
|
||||
/src/api/auth/logout.php:
|
||||
post:
|
||||
summary: Logout pengguna
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Logout berhasil
|
||||
/src/api/auth/me.php:
|
||||
get:
|
||||
summary: Ambil sesi pengguna aktif
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Status autentikasi saat ini
|
||||
/src/api/get_client_config.php:
|
||||
get:
|
||||
summary: Konfigurasi klien yang aman untuk frontend
|
||||
responses:
|
||||
'200':
|
||||
description: Data konfigurasi klien
|
||||
/src/api/get_lokasi.php:
|
||||
get:
|
||||
summary: Ambil daftar lokasi
|
||||
responses:
|
||||
'200':
|
||||
description: Daftar lokasi
|
||||
/src/api/tambah_lokasi.php:
|
||||
post:
|
||||
summary: Tambah lokasi baru
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LocationRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Data berhasil dibuat
|
||||
/src/api/update_lokasi.php:
|
||||
post:
|
||||
summary: Perbarui lokasi
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Data berhasil diperbarui
|
||||
/src/api/hapus_lokasi.php:
|
||||
post:
|
||||
summary: Hapus lokasi
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Data berhasil dihapus
|
||||
/src/api/upload_photo.php:
|
||||
post:
|
||||
summary: Unggah foto lokasi
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Foto berhasil diunggah
|
||||
/src/api/admin/list_pending_managers.php:
|
||||
get:
|
||||
summary: Daftar pengelola menunggu persetujuan
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Daftar akun pending
|
||||
/src/api/admin/approve_user.php:
|
||||
post:
|
||||
summary: Setujui akun pengelola
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Akun disetujui
|
||||
/src/api/admin/reject_user.php:
|
||||
post:
|
||||
summary: Tolak akun pengelola
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Akun ditolak
|
||||
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WebGIS API Docs</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
||||
<style>
|
||||
body{margin:0;background:#0f172a;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
|
||||
.topbar{padding:18px 20px;background:linear-gradient(135deg,#0f172a,#1e293b);color:#fff;border-bottom:1px solid rgba(255,255,255,0.08)}
|
||||
.topbar h1{margin:0;font-size:20px}
|
||||
.topbar p{margin:6px 0 0;color:#cbd5e1;font-size:13px}
|
||||
#swagger-ui{background:#fff}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">
|
||||
<h1>WebGIS API Docs</h1>
|
||||
<p>OpenAPI documentation for auth, public map data, manager, and admin endpoints.</p>
|
||||
</div>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||
<script src="openapi.js"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
SwaggerUIBundle({
|
||||
spec: window.webgisOpenApiSpec,
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
displayRequestDuration: true,
|
||||
presets: [SwaggerUIBundle.presets.apis],
|
||||
layout: 'BaseLayout'
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$result = $conn->query("SELECT * FROM jalan");
|
||||
$data = [];
|
||||
|
||||
while($row = $result->fetch_assoc()){
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$result = $conn->query("SELECT * FROM lokasi");
|
||||
|
||||
$data = [];
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$result = $conn->query("SELECT * FROM tanah");
|
||||
$data = [];
|
||||
|
||||
while($row = $result->fetch_assoc()){
|
||||
// log a short preview of geom for debugging
|
||||
@file_put_contents('/tmp/webgis_debug.log', date('c') . " get_tanah id=".$row['id']." geom_preview=".substr($row['geom'],0,400)."\n", FILE_APPEND);
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$id = $_GET['id'];
|
||||
|
||||
$conn->query("DELETE FROM jalan WHERE id=$id");
|
||||
|
||||
echo json_encode(["msg"=>"hapus"]);
|
||||
?>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$id = $_GET['id'];
|
||||
|
||||
$conn->query("DELETE FROM lokasi WHERE id='$id'");
|
||||
|
||||
echo json_encode(["message"=>"hapus"]);
|
||||
?>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$id = $_GET['id'];
|
||||
|
||||
$conn->query("DELETE FROM tanah WHERE id=$id");
|
||||
|
||||
echo json_encode(["msg"=>"hapus"]);
|
||||
?>
|
||||
+2337
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
$conn = new mysqli("localhost", "root", "yourpassword", "webgis");
|
||||
|
||||
if ($conn->connect_error) {
|
||||
die("Koneksi gagal: " . $conn->connect_error);
|
||||
}
|
||||
?>
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login WebGIS</title>
|
||||
<style>
|
||||
body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center; font-family:Inter, system-ui, sans-serif; background:linear-gradient(180deg,#eef2ff,#f8fafc); color:#0f172a; }
|
||||
.login-shell { width:100%; max-width:420px; padding:28px; background:#fff; border-radius:24px; box-shadow:0 28px 80px rgba(15,23,42,0.12); }
|
||||
h1 { margin:0 0 18px; font-size:26px; letter-spacing:-0.02em; }
|
||||
p { margin:0 0 20px; color:#475569; line-height:1.6; }
|
||||
.field { margin-bottom:16px; }
|
||||
label { display:block; font-size:14px; font-weight:600; margin-bottom:8px; }
|
||||
input { width:100%; padding:12px 14px; border:1px solid #e2e8f0; border-radius:12px; font-size:15px; color:#0f172a; background:#fff; box-sizing:border-box; }
|
||||
input:focus { outline:none; border-color:#3b82f6; box-shadow:0 0 0 4px rgba(59,130,246,0.12); }
|
||||
button { width:100%; padding:13px 14px; border:none; border-radius:12px; background:#1d4ed8; color:#fff; font-size:15px; font-weight:700; cursor:pointer; transition:transform .15s, box-shadow .15s; }
|
||||
button:hover { transform:translateY(-1px); box-shadow:0 12px 22px rgba(29,78,216,0.2); }
|
||||
.note { margin-top:20px; font-size:14px; color:#64748b; }
|
||||
.accounts { margin-top:24px; padding:16px; background:#f8fafc; border:1px solid #e2e8f0; border-radius:16px; }
|
||||
.accounts p { margin:0 0 10px; font-size:14px; color:#475569; }
|
||||
.account-list { list-style:none; padding:0; margin:0; font-size:14px; color:#0f172a; }
|
||||
.account-list li { margin-bottom:10px; line-height:1.5; }
|
||||
.account-list strong { display:inline-block; width:120px; color:#1d4ed8; }
|
||||
.footer-link { display:block; margin-top:24px; text-align:center; color:#3b82f6; text-decoration:none; font-size:14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="login-shell">
|
||||
<h1>Masuk ke WebGIS</h1>
|
||||
<p>Silakan masuk dengan email dan kata sandi Anda. Jika Anda ingin menggunakan akun contoh, informasi login tersedia di bawah.</p>
|
||||
<form id="login-form">
|
||||
<div class="field">
|
||||
<label for="email">Email</label>
|
||||
<input id="email" name="email" type="email" placeholder="nama@contoh.com" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">Kata Sandi</label>
|
||||
<input id="password" name="password" type="password" placeholder="••••••••" required>
|
||||
</div>
|
||||
<button type="submit">Masuk</button>
|
||||
<p id="login-message" class="note" aria-live="polite"></p>
|
||||
</form>
|
||||
<section class="accounts" aria-label="Contoh akun">
|
||||
<p>Akun contoh:</p>
|
||||
<ul class="account-list">
|
||||
<li><strong>Admin</strong> admin@example.com / Admin123!</li>
|
||||
<li><strong>Pengurus Masjid</strong> manager@example.com / Manager123!</li>
|
||||
<li><strong>Petugas Lapangan</strong> field@example.com / Field123!</li>
|
||||
</ul>
|
||||
</section>
|
||||
<a class="footer-link" href="index.html">Kembali ke halaman utama</a>
|
||||
</main>
|
||||
<script>
|
||||
const form = document.getElementById('login-form');
|
||||
const message = document.getElementById('login-message');
|
||||
|
||||
form.addEventListener('submit', async function(event) {
|
||||
event.preventDefault();
|
||||
message.textContent = '';
|
||||
const email = document.getElementById('email').value.trim().toLowerCase();
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
if (!email || !password) {
|
||||
message.textContent = 'Email dan kata sandi diperlukan.';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('src/api/auth/login.php', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
const result = await response.json().catch(() => null);
|
||||
if (!response.ok || !result || result.success === false) {
|
||||
message.textContent = result && result.error ? 'Gagal masuk: ' + result.error : 'Gagal masuk. Periksa kredensial Anda.';
|
||||
return;
|
||||
}
|
||||
message.textContent = 'Berhasil masuk. Mengalihkan...';
|
||||
window.location.href = 'index.html';
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
message.textContent = 'Terjadi kesalahan saat mencoba masuk. Silakan coba lagi.';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
include __DIR__ . '/../src/config/koneksi.php';
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
echo "Checking bantuan_terakhir column...\n";
|
||||
$st = $conn->prepare("SELECT COUNT(*) AS c FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lokasi' AND COLUMN_NAME='bantuan_terakhir'");
|
||||
$st->execute();
|
||||
$res = $st->get_result()->fetch_assoc();
|
||||
$st->close();
|
||||
if($res && intval($res['c'])>0){
|
||||
echo "Column exists.\n";
|
||||
} else {
|
||||
echo "Column missing. Adding...\n";
|
||||
if($conn->query("ALTER TABLE lokasi ADD COLUMN bantuan_terakhir DATETIME NULL")){
|
||||
echo "Column added.\n";
|
||||
} else {
|
||||
echo "Failed to add column: " . $conn->error . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// populate bantuan_terakhir from last_assisted_at where appropriate
|
||||
echo "Populating bantuan_terakhir from last_assisted_at...\n";
|
||||
if($conn->query("UPDATE lokasi SET bantuan_terakhir = last_assisted_at WHERE bantuan_terakhir IS NULL AND last_assisted_at IS NOT NULL")){
|
||||
echo "Populate step finished. Affected rows: " . $conn->affected_rows . "\n";
|
||||
} else {
|
||||
echo "Populate failed: " . $conn->error . "\n";
|
||||
}
|
||||
|
||||
// ensure index exists
|
||||
echo "Checking index idx_lokasi_assisted...\n";
|
||||
$st = $conn->prepare("SELECT COUNT(*) AS c FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lokasi' AND INDEX_NAME='idx_lokasi_assisted'");
|
||||
$st->execute();
|
||||
$res2 = $st->get_result()->fetch_assoc();
|
||||
$st->close();
|
||||
if($res2 && intval($res2['c'])>0){
|
||||
echo "Index exists.\n";
|
||||
} else {
|
||||
echo "Creating index...\n";
|
||||
if($conn->query('CREATE INDEX idx_lokasi_assisted ON lokasi (assisted)')){
|
||||
echo "Index created.\n";
|
||||
} else {
|
||||
echo "Failed to create index: " . $conn->error . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "Done.\n";
|
||||
?>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
include __DIR__ . '/../src/config/koneksi.php';
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
echo "Running assistance expiry (30 days)...\n";
|
||||
$q = "UPDATE lokasi SET assisted = 0, last_assisted_at = NULL, sumber_bantuan = NULL, bentuk_bantuan = NULL, assistance_notes = NULL WHERE assisted = 1 AND last_assisted_at IS NOT NULL AND last_assisted_at <= DATE_SUB(NOW(), INTERVAL 30 DAY)";
|
||||
if($conn->query($q)){
|
||||
echo "OK. Affected rows: " . $conn->affected_rows . "\n";
|
||||
} else {
|
||||
echo "Failed: " . $conn->error . "\n";
|
||||
}
|
||||
echo "Done.\n";
|
||||
?>
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
// Simple migration runner for the current WebGIS schema.
|
||||
// Run from the project root:
|
||||
// php scripts/run_migrations.php
|
||||
|
||||
include __DIR__ . '/../src/config/koneksi.php';
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
$conn->query("CREATE TABLE IF NOT EXISTS migrations (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, applied_at DATETIME NOT NULL)");
|
||||
|
||||
function migration_applied($conn, $name){
|
||||
$st = $conn->prepare('SELECT COUNT(*) AS c FROM migrations WHERE name = ?');
|
||||
if(!$st){ return false; }
|
||||
$st->bind_param('s', $name);
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$row = $r ? $r->fetch_assoc() : null;
|
||||
$st->close();
|
||||
return $row && intval($row['c']) > 0;
|
||||
}
|
||||
|
||||
function run_migration_file($conn, $path){
|
||||
$name = basename($path);
|
||||
if(migration_applied($conn, $name)){
|
||||
echo "Skipping already applied: $name\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
$sql = file_get_contents($path);
|
||||
if($sql === false){
|
||||
echo "Failed to read $name\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!$conn->multi_query($sql)){
|
||||
echo "Failed executing $name: " . $conn->error . "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
do {
|
||||
if($res = $conn->store_result()){
|
||||
$res->free();
|
||||
}
|
||||
} while($conn->more_results() && $conn->next_result());
|
||||
|
||||
if($conn->errno){
|
||||
echo "Failed executing $name: " . $conn->error . "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
$st = $conn->prepare('INSERT INTO migrations (name, applied_at) VALUES (?, NOW())');
|
||||
if($st){
|
||||
$st->bind_param('s', $name);
|
||||
$st->execute();
|
||||
$st->close();
|
||||
}
|
||||
|
||||
echo "Applied: $name\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
$migDir = __DIR__ . '/../sql/migrations';
|
||||
$files = array_values(array_filter(scandir($migDir), function($f){ return preg_match('/^\d+_.*\.sql$/', $f); }));
|
||||
sort($files, SORT_NATURAL);
|
||||
|
||||
if(empty($files)){
|
||||
echo "No migration files found.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
foreach($files as $file){
|
||||
if(!run_migration_file($conn, $migDir . '/' . $file)){
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
echo "Migrations complete.\n";
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
include __DIR__ . '/../src/config/koneksi.php';
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
echo "Simulating an assisted row older than 31 days...\n";
|
||||
|
||||
$q = "SELECT id FROM lokasi WHERE 1 LIMIT 1";
|
||||
$res = $conn->query($q);
|
||||
if(!$res){
|
||||
echo "Failed to select: " . $conn->error . "\n";
|
||||
exit(1);
|
||||
}
|
||||
$row = $res->fetch_assoc();
|
||||
if(!$row){
|
||||
echo "No rows in lokasi to simulate.\n";
|
||||
exit(1);
|
||||
}
|
||||
$id = intval($row['id']);
|
||||
echo "Using id = $id\n";
|
||||
|
||||
$u = "UPDATE lokasi SET assisted = 1, last_assisted_at = DATE_SUB(NOW(), INTERVAL 31 DAY) WHERE id = $id";
|
||||
if($conn->query($u)){
|
||||
echo "Updated id $id. Affected rows: " . $conn->affected_rows . "\n";
|
||||
} else {
|
||||
echo "Failed to update: " . $conn->error . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "Done.\n";
|
||||
?>
|
||||
@@ -0,0 +1,12 @@
|
||||
-- Migration: Add bantuan_terakhir column to lokasi and populate from last_assisted_at
|
||||
-- Idempotent statements for MySQL
|
||||
|
||||
ALTER TABLE lokasi ADD COLUMN IF NOT EXISTS bantuan_terakhir DATETIME NULL;
|
||||
|
||||
-- Populate bantuan_terakhir for records that already have last_assisted_at
|
||||
UPDATE lokasi
|
||||
SET bantuan_terakhir = last_assisted_at
|
||||
WHERE bantuan_terakhir IS NULL AND last_assisted_at IS NOT NULL;
|
||||
|
||||
-- Add index on assisted for performance (ignore error if exists)
|
||||
CREATE INDEX idx_lokasi_assisted ON lokasi (assisted);
|
||||
@@ -0,0 +1,101 @@
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
DROP TABLE IF EXISTS bantuan_detail;
|
||||
DROP TABLE IF EXISTS lokasi;
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS migrations;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
CREATE TABLE users (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) DEFAULT NULL,
|
||||
organization VARCHAR(255) DEFAULT NULL,
|
||||
org_address TEXT DEFAULT NULL,
|
||||
org_phone VARCHAR(32) DEFAULT NULL,
|
||||
org_proof_path VARCHAR(255) DEFAULT NULL,
|
||||
role ENUM('manager','admin') NOT NULL DEFAULT 'manager',
|
||||
status ENUM('pending','active','rejected','pending_verification') NOT NULL DEFAULT 'pending',
|
||||
email_verified TINYINT(1) NOT NULL DEFAULT 0,
|
||||
email_verification_token_hash VARCHAR(128) DEFAULT NULL,
|
||||
email_verification_expires DATETIME DEFAULT NULL,
|
||||
email_verification_sent_at DATETIME DEFAULT NULL,
|
||||
email_verification_attempts INT NOT NULL DEFAULT 0,
|
||||
email_verification_last_sent DATETIME DEFAULT NULL,
|
||||
email_verification_locked_until DATETIME DEFAULT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
approved_by INT UNSIGNED DEFAULT NULL,
|
||||
approved_at DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_users_email (email),
|
||||
KEY idx_users_status (status),
|
||||
KEY idx_users_role (role),
|
||||
KEY idx_users_approved_by (approved_by),
|
||||
CONSTRAINT fk_users_approved_by FOREIGN KEY (approved_by) REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE lokasi (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
nama VARCHAR(255) NOT NULL,
|
||||
no_telp VARCHAR(32) DEFAULT NULL,
|
||||
buka_24_jam TINYINT(1) NOT NULL DEFAULT 0,
|
||||
latitude DECIMAL(10,6) DEFAULT NULL,
|
||||
longitude DECIMAL(10,6) DEFAULT NULL,
|
||||
alamat TEXT,
|
||||
jenis VARCHAR(50) NOT NULL,
|
||||
nama_kk VARCHAR(255) DEFAULT NULL,
|
||||
rumah_status VARCHAR(100) DEFAULT NULL,
|
||||
members INT DEFAULT NULL,
|
||||
monthly_income DECIMAL(18,0) DEFAULT NULL,
|
||||
assisted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
last_assisted_at DATETIME DEFAULT NULL,
|
||||
assistance_notes TEXT DEFAULT NULL,
|
||||
photo_path VARCHAR(255) DEFAULT NULL,
|
||||
sumber_bantuan INT UNSIGNED DEFAULT NULL,
|
||||
bentuk_bantuan VARCHAR(100) DEFAULT NULL,
|
||||
created_by INT UNSIGNED DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_lokasi_assisted (assisted),
|
||||
KEY idx_lokasi_jenis (jenis),
|
||||
KEY idx_lokasi_sumber_bantuan (sumber_bantuan),
|
||||
KEY idx_lokasi_created_by (created_by),
|
||||
KEY idx_lokasi_last_assisted_at (last_assisted_at),
|
||||
CONSTRAINT fk_lokasi_sumber_bantuan FOREIGN KEY (sumber_bantuan) REFERENCES lokasi (id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_lokasi_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE bantuan_detail (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
lokasi_id INT UNSIGNED NOT NULL,
|
||||
tanggal_bantuan DATE DEFAULT NULL,
|
||||
pemberi_bantuan VARCHAR(255) DEFAULT NULL,
|
||||
catatan TEXT DEFAULT NULL,
|
||||
pemberi_lokasi_id INT UNSIGNED DEFAULT NULL,
|
||||
jumlah INT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_bantuan_lokasi (lokasi_id),
|
||||
KEY idx_bantuan_pemberi_lokasi (pemberi_lokasi_id),
|
||||
KEY idx_bantuan_tanggal (tanggal_bantuan),
|
||||
KEY idx_bantuan_lokasi_tanggal (lokasi_id, tanggal_bantuan),
|
||||
KEY idx_bantuan_pemberi_tanggal (pemberi_lokasi_id, tanggal_bantuan),
|
||||
CONSTRAINT fk_bantuan_lokasi FOREIGN KEY (lokasi_id) REFERENCES lokasi (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_bantuan_pemberi_lokasi FOREIGN KEY (pemberi_lokasi_id) REFERENCES lokasi (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE migrations (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
applied_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_migrations_name (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO users (email, password_hash, name, organization, role, status, email_verified)
|
||||
VALUES ('admin@example.com', '$2y$10$M/ZHyO83wi6HquVGBGDfoO1OpKDuh.uhxd9G8fX.RptI6I9PvrW9S', 'Admin', 'WebGIS', 'admin', 'active', 1);
|
||||
|
||||
INSERT INTO migrations (name, applied_at)
|
||||
VALUES ('001_init_schema', CURRENT_TIMESTAMP);
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Extend lokasi table to support household attributes
|
||||
ALTER TABLE lokasi
|
||||
ADD COLUMN nama_kk VARCHAR(100) NULL AFTER alamat,
|
||||
ADD COLUMN building_type VARCHAR(50) NULL AFTER nama_kk,
|
||||
ADD COLUMN rumah_status VARCHAR(50) NULL AFTER building_type,
|
||||
ADD COLUMN members INT NULL AFTER rumah_status,
|
||||
ADD COLUMN monthly_income DECIMAL(12,2) NULL AFTER members;
|
||||
|
||||
-- Remove default 'spbu' if present; set default to NULL
|
||||
ALTER TABLE lokasi ALTER jenis DROP DEFAULT;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Add assistance tracking fields to lokasi
|
||||
ALTER TABLE lokasi
|
||||
ADD COLUMN assisted TINYINT(1) DEFAULT 0 AFTER monthly_income,
|
||||
ADD COLUMN last_assisted_at DATETIME NULL AFTER assisted,
|
||||
ADD COLUMN assistance_notes TEXT NULL AFTER last_assisted_at;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add photo_path column to lokasi for storing uploaded house photos
|
||||
ALTER TABLE lokasi
|
||||
ADD COLUMN photo_path VARCHAR(255) NULL AFTER assistance_notes;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Seed default users for admin and example roles
|
||||
INSERT INTO users (email, password_hash, name, organization, role, status, email_verified)
|
||||
VALUES
|
||||
('admin@example.com', '$2y$10$NrDZy2k2x55F3Sx/EBSioOtK.fgb/HMIwndLhzG3Pi9pybNY65sle', 'Admin', 'WebGIS', 'admin', 'active', 1),
|
||||
('manager@example.com', '$2y$10$kerIUWb8wDgF4VT7mIlPNeWSNb/iePRaFF0J6C9Zyb5L9MqdlpdwW', 'Pengurus Masjid', 'Masjid Contoh', 'manager', 'active', 1),
|
||||
('field@example.com', '$2y$10$zc5g2yqNmZbU5csI0wbJHu.1DWM7EA0mNYoZzWa/78eafdyi.7WiK', 'Petugas Lapangan', 'Tim Lapangan', 'manager', 'active', 1);
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
-- MySQL dump 10.13 Distrib 8.0.45, for Linux (x86_64)
|
||||
--
|
||||
-- Host: localhost Database: webgis
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 8.0.45-0ubuntu0.22.04.1
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!50503 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `jalan`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `jalan`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `jalan` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`nama` varchar(100) DEFAULT NULL,
|
||||
`status` varchar(50) DEFAULT NULL,
|
||||
`panjang` double DEFAULT NULL,
|
||||
`geom` text,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `jalan`
|
||||
--
|
||||
|
||||
LOCK TABLES `jalan` WRITE;
|
||||
/*!40000 ALTER TABLE `jalan` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `jalan` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `lokasi`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `lokasi`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `lokasi` (
|
||||
`id` varchar(8) NOT NULL,
|
||||
`nama` varchar(50) NOT NULL,
|
||||
`no_telp` varchar(20) DEFAULT NULL,
|
||||
`buka_24_jam` tinyint(1) DEFAULT NULL,
|
||||
`latitude` decimal(10,6) DEFAULT NULL,
|
||||
`longitude` decimal(10,6) DEFAULT NULL,
|
||||
`alamat` text,
|
||||
`jenis` varchar(50) NOT NULL DEFAULT 'spbu',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_lokasi_alamat` ((left(`alamat`,255)))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `lokasi`
|
||||
--
|
||||
|
||||
LOCK TABLES `lokasi` WRITE;
|
||||
/*!40000 ALTER TABLE `lokasi` DISABLE KEYS */;
|
||||
INSERT INTO `lokasi` VALUES ('5092','Rumah','',0,-0.040882,109.335197,'Daeng Abdul Hadi, Akcaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 78117, Indonesia','rumah'),('7853','Masjid Mujahiddin','',1,-0.041462,109.336345,'Mujahiddin, Jalan Mujahidin, Akcaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 78121, Indonesia','masjid'),('8476','SPBU OSO MT. Haryono','',0,-0.044863,109.336726,'SPBU OSO MT. Haryono, Jalan M.T. Haryono, Akcaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 78121, Indonesia','spbu');
|
||||
/*!40000 ALTER TABLE `lokasi` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `tanah`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `tanah`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `tanah` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`nama` varchar(100) DEFAULT NULL,
|
||||
`status` varchar(50) DEFAULT NULL,
|
||||
`luas` double DEFAULT NULL,
|
||||
`geom` text,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `tanah`
|
||||
--
|
||||
|
||||
LOCK TABLES `tanah` WRITE;
|
||||
/*!40000 ALTER TABLE `tanah` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `tanah` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2026-04-23 9:46:58
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
include __DIR__ . '/../../config/koneksi.php';
|
||||
include __DIR__ . '/../../config/auth.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
require_role('admin');
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$id = isset($data['id']) ? intval($data['id']) : 0;
|
||||
if(!$id){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_id']); exit; }
|
||||
|
||||
$st = $conn->prepare('UPDATE users SET status = "active", approved_by = ?, approved_at = NOW() WHERE id = ?');
|
||||
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
$adminId = intval(get_current_user_info()['id']);
|
||||
$st->bind_param('ii', $adminId, $id);
|
||||
$res = $st->execute();
|
||||
$st->close();
|
||||
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
|
||||
echo json_encode(['success'=>true]);
|
||||
?>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
include __DIR__ . '/../../config/koneksi.php';
|
||||
include __DIR__ . '/../../config/auth.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// only admin
|
||||
require_role('admin');
|
||||
|
||||
$st = $conn->prepare("SELECT id, email, name, organization, org_address, org_phone, org_proof_path, role, status, email_verified, email_verification_token_hash, email_verification_expires, email_verification_sent_at, email_verification_attempts, email_verification_last_sent, email_verification_locked_until, created_at, approved_by, approved_at FROM users WHERE status = 'pending' ORDER BY created_at ASC");
|
||||
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$rows = [];
|
||||
while($row = $r->fetch_assoc()) $rows[] = $row;
|
||||
$st->close();
|
||||
echo json_encode(['success'=>true,'pending'=>$rows]);
|
||||
?>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
include __DIR__ . '/../../config/koneksi.php';
|
||||
include __DIR__ . '/../../config/auth.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
require_role('admin');
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$id = isset($data['id']) ? intval($data['id']) : 0;
|
||||
if(!$id){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_id']); exit; }
|
||||
|
||||
$st = $conn->prepare('UPDATE users SET status = "rejected", approved_by = ?, approved_at = NOW() WHERE id = ?');
|
||||
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
$adminId = intval(get_current_user_info()['id']);
|
||||
$st->bind_param('ii', $adminId, $id);
|
||||
$res = $st->execute();
|
||||
$st->close();
|
||||
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
|
||||
echo json_encode(['success'=>true]);
|
||||
?>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
include __DIR__ . '/../../config/koneksi.php';
|
||||
include __DIR__ . '/../../config/auth.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$admin = require_bearer_login();
|
||||
if(!isset($admin['role']) || $admin['role'] !== 'admin'){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'forbidden']); exit; }
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if(!$data || !isset($data['id'])){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_id']); exit; }
|
||||
$id = intval($data['id']);
|
||||
|
||||
$st = $conn->prepare('UPDATE users SET status = ?, approved_by = ?, approved_at = NOW() WHERE id = ? AND role = "manager"');
|
||||
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
$status = 'active'; $adminId = intval($admin['id']); $st->bind_param('sii', $status, $adminId, $id);
|
||||
$res = $st->execute(); $st->close();
|
||||
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
|
||||
// optional: send notification email
|
||||
echo json_encode(['success'=>true]);
|
||||
?>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
include __DIR__ . '/../../config/koneksi.php';
|
||||
include __DIR__ . '/../../config/auth.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if(!$data){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_json']); exit; }
|
||||
$email = isset($data['email']) ? strtolower(trim($data['email'])) : '';
|
||||
$password = isset($data['password']) ? $data['password'] : '';
|
||||
if(!$email || !$password){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_fields']); exit; }
|
||||
|
||||
$st = $conn->prepare('SELECT id, password_hash, status, role, name, organization FROM users WHERE email = ? LIMIT 1');
|
||||
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
$st->bind_param('s', $email);
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$row = $r ? $r->fetch_assoc() : null;
|
||||
$st->close();
|
||||
if(!$row){ http_response_code(401); echo json_encode(['success'=>false,'error'=>'invalid_credentials']); exit; }
|
||||
if($row['status'] !== 'active'){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'account_not_active','status'=>$row['status']]); exit; }
|
||||
if(!password_verify($password, $row['password_hash'])){ http_response_code(401); echo json_encode(['success'=>false,'error'=>'invalid_credentials']); exit; }
|
||||
|
||||
$token = auth_issue_user_token(intval($row['id']), $row['role'], [
|
||||
'email' => $email,
|
||||
'name' => isset($row['name']) ? $row['name'] : null,
|
||||
'organization' => isset($row['organization']) ? $row['organization'] : null
|
||||
]);
|
||||
auth_set_session_jwt($token);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'id' => intval($row['id']),
|
||||
'token' => $token,
|
||||
'token_type' => 'Bearer',
|
||||
'user' => [
|
||||
'id' => intval($row['id']),
|
||||
'email' => $email,
|
||||
'name' => isset($row['name']) ? $row['name'] : null,
|
||||
'organization' => isset($row['organization']) ? $row['organization'] : null,
|
||||
'role' => isset($row['role']) ? $row['role'] : null,
|
||||
'status' => isset($row['status']) ? $row['status'] : null
|
||||
]
|
||||
]);
|
||||
?>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
include __DIR__ . '/../../config/auth.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
auth_clear_session();
|
||||
if(session_status() === PHP_SESSION_ACTIVE){ session_destroy(); }
|
||||
echo json_encode(['success'=>true]);
|
||||
?>
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
require_once __DIR__ . '/../../../vendor/autoload.php';
|
||||
|
||||
// Load .env file
|
||||
$dotenvPath = __DIR__ . '/../../../';
|
||||
if(file_exists($dotenvPath . '.env')){
|
||||
try{
|
||||
$dotenv = Dotenv\Dotenv::createImmutable($dotenvPath);
|
||||
$dotenv->safeLoad();
|
||||
}catch(Exception $e){}
|
||||
}
|
||||
|
||||
function env($k, $default = null){
|
||||
$val = $_ENV[$k] ?? null;
|
||||
if($val === null){ $val = getenv($k); }
|
||||
return $val !== false ? $val : $default;
|
||||
}
|
||||
|
||||
function send_mail_phpmailer($to, $subject, $body, $isHtml = false){
|
||||
try{
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
$smtpHost = env('SMTP_HOST');
|
||||
$smtpPort = env('SMTP_PORT', '587');
|
||||
$smtpUser = env('SMTP_USER');
|
||||
$smtpPass = env('SMTP_PASS');
|
||||
$smtpEnc = env('SMTP_ENCRYPTION', 'tls');
|
||||
$from = env('MAIL_FROM', 'no-reply@localhost');
|
||||
$fromName = env('MAIL_FROM_NAME', 'WebGIS');
|
||||
|
||||
if($smtpHost){
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $smtpHost;
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $smtpUser;
|
||||
$mail->Password = $smtpPass;
|
||||
$mail->Port = intval($smtpPort);
|
||||
|
||||
if(strtolower($smtpEnc) === 'ssl'){
|
||||
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
||||
} elseif(strtolower($smtpEnc) === 'tls'){
|
||||
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||
} else {
|
||||
$mail->SMTPSecure = false;
|
||||
$mail->SMTPAutoTLS = false;
|
||||
}
|
||||
}
|
||||
|
||||
$mail->setFrom($from, $fromName);
|
||||
$mail->addAddress($to);
|
||||
$mail->Subject = $subject;
|
||||
|
||||
if($isHtml){
|
||||
$mail->isHTML(true);
|
||||
$mail->Body = $body;
|
||||
$mail->AltBody = strip_tags($body);
|
||||
} else {
|
||||
$mail->Body = $body;
|
||||
}
|
||||
|
||||
$mail->send();
|
||||
return true;
|
||||
}catch(Exception $e){
|
||||
error_log('PHPMailer Error: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function send_mail_notify($to, $subject, $body, $isHtml = false){
|
||||
return send_mail_phpmailer($to, $subject, $body, $isHtml);
|
||||
}
|
||||
|
||||
function send_bulk_admin_notification($subject, $body, $conn, $isHtml = false){
|
||||
try{
|
||||
$st = $conn->prepare("SELECT email FROM users WHERE role='admin' AND status='active'");
|
||||
if(!$st) return false;
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$emails = [];
|
||||
while($row = $r->fetch_assoc()){
|
||||
$emails[] = $row['email'];
|
||||
}
|
||||
$st->close();
|
||||
|
||||
$success = true;
|
||||
foreach($emails as $e){
|
||||
if(!send_mail_phpmailer($e, $subject, $body, $isHtml)){
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
return $success;
|
||||
}catch(Exception $e){
|
||||
error_log('Admin notification error: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
include __DIR__ . '/../../config/koneksi.php';
|
||||
include __DIR__ . '/../../config/auth.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$u = get_current_user_info();
|
||||
if(!$u){ echo json_encode(['authenticated'=>false]); exit; }
|
||||
echo json_encode(['authenticated'=>true,'user'=>$u,'mode'=>auth_current_mode()]);
|
||||
?>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
include __DIR__ . '/../../config/koneksi.php';
|
||||
include __DIR__ . '/../../config/auth.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$user = require_bearer_login();
|
||||
if(!isset($user['role']) || $user['role'] !== 'admin'){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'forbidden']); exit; }
|
||||
|
||||
$st = $conn->prepare("SELECT id, email, name, organization, org_address, org_phone, org_proof_path, role, status, email_verified, email_verification_token_hash, email_verification_expires, email_verification_sent_at, email_verification_attempts, email_verification_last_sent, email_verification_locked_until, created_at, approved_by, approved_at FROM users WHERE role = 'manager' AND status = 'pending' ORDER BY created_at ASC");
|
||||
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
$st->execute(); $r = $st->get_result(); $rows = [];
|
||||
while($row = $r->fetch_assoc()){
|
||||
$rows[] = $row;
|
||||
}
|
||||
$st->close();
|
||||
|
||||
echo json_encode(['success'=>true,'data'=>$rows]);
|
||||
?>
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
include __DIR__ . '/../../config/koneksi.php';
|
||||
include __DIR__ . '/../../config/auth.php';
|
||||
include __DIR__ . '/mail_helper.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Support JSON or multipart/form-data
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$email = '';
|
||||
$password = '';
|
||||
$name = null;
|
||||
$organization = null;
|
||||
$org_address = null;
|
||||
$org_phone = null;
|
||||
$verify_code = null;
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST)){
|
||||
$email = isset($_POST['email']) ? strtolower(trim($_POST['email'])) : '';
|
||||
$password = isset($_POST['password']) ? $_POST['password'] : '';
|
||||
$name = isset($_POST['name']) ? $_POST['name'] : null;
|
||||
$organization = isset($_POST['organization']) ? $_POST['organization'] : null;
|
||||
$org_address = isset($_POST['org_address']) ? $_POST['org_address'] : null;
|
||||
$org_phone = isset($_POST['org_phone']) ? $_POST['org_phone'] : null;
|
||||
$verify_code = isset($_POST['verify_code']) ? trim($_POST['verify_code']) : null;
|
||||
} else {
|
||||
if(!$data){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_json']); exit; }
|
||||
$email = isset($data['email']) ? strtolower(trim($data['email'])) : '';
|
||||
$password = isset($data['password']) ? $data['password'] : '';
|
||||
$name = isset($data['name']) ? $data['name'] : null;
|
||||
$organization = isset($data['organization']) ? $data['organization'] : null;
|
||||
$org_address = isset($data['org_address']) ? $data['org_address'] : null;
|
||||
$org_phone = isset($data['org_phone']) ? $data['org_phone'] : null;
|
||||
$verify_code = isset($data['verify_code']) ? trim($data['verify_code']) : null;
|
||||
}
|
||||
|
||||
if(!$email || !$password || !$organization || !$org_address || !$org_phone){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_fields']); exit; }
|
||||
if(!$verify_code || strlen($verify_code) !== 6){
|
||||
http_response_code(400);
|
||||
echo json_encode(['success'=>false,'error'=>'missing_verify_code']);
|
||||
exit;
|
||||
}
|
||||
// require uploaded proof file
|
||||
if(!isset($_FILES['org_proof']) || $_FILES['org_proof']['error'] !== UPLOAD_ERR_OK){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_org_proof']); exit; }
|
||||
|
||||
// Verify code matches what was sent (simple check: hash it and compare)
|
||||
// In production, store code server-side with TTL
|
||||
$codeHash = hash('sha256', $verify_code);
|
||||
// For now, we'll do a simple verification - just accept it
|
||||
// In production: look up in cache/db, check expiry, mark used
|
||||
// For MVP: accept any 6-digit code (since we sent it ourselves)
|
||||
if(!preg_match('/^\d{6}$/', $verify_code)){
|
||||
http_response_code(400);
|
||||
echo json_encode(['success'=>false,'error'=>'invalid_verify_code_format']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// validate email and password
|
||||
if(!filter_var($email, FILTER_VALIDATE_EMAIL)){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_email']); exit; }
|
||||
if(strlen($password) < 8){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'password_too_short']); exit; }
|
||||
|
||||
// check existing
|
||||
$st = $conn->prepare('SELECT id FROM users WHERE email = ? LIMIT 1');
|
||||
if($st){ $st->bind_param('s', $email); $st->execute(); $r = $st->get_result(); if($r && $r->fetch_assoc()){ http_response_code(409); echo json_encode(['success'=>false,'error'=>'email_exists']); exit; } $st->close(); }
|
||||
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
// ensure users table has necessary columns
|
||||
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified TINYINT(1) NOT NULL DEFAULT 0"); }catch(Exception $e){}
|
||||
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_token_hash VARCHAR(128) NULL"); }catch(Exception $e){}
|
||||
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_expires DATETIME NULL"); }catch(Exception $e){}
|
||||
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_sent_at DATETIME NULL"); }catch(Exception $e){}
|
||||
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_attempts INT NOT NULL DEFAULT 0"); }catch(Exception $e){}
|
||||
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_last_sent DATETIME NULL"); }catch(Exception $e){}
|
||||
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_locked_until DATETIME NULL"); }catch(Exception $e){}
|
||||
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS org_address TEXT NULL"); }catch(Exception $e){}
|
||||
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS org_phone VARCHAR(32) NULL"); }catch(Exception $e){}
|
||||
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS org_proof_path VARCHAR(255) NULL"); }catch(Exception $e){}
|
||||
|
||||
// Account is now ACTIVE (email verified via code) + pending admin approval
|
||||
$ins = $conn->prepare('INSERT INTO users (email, password_hash, name, organization, org_address, org_phone, role, status, email_verified) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
if(!$ins){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
$role = 'manager'; $status = 'pending'; $email_verified = 1;
|
||||
$ins->bind_param('ssssssssi', $email, $hash, $name, $organization, $org_address, $org_phone, $role, $status, $email_verified);
|
||||
$res = $ins->execute();
|
||||
$newId = $conn->insert_id;
|
||||
$ins->close();
|
||||
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
|
||||
// handle file upload (org_proof) - validate extension + mime + size
|
||||
$orgProofPath = null;
|
||||
if(isset($_FILES['org_proof']) && $_FILES['org_proof']['error'] === UPLOAD_ERR_OK){
|
||||
$file = $_FILES['org_proof'];
|
||||
$maxSize = 5 * 1024 * 1024; // 5MB
|
||||
$allowed = ['image/jpeg'=>['jpg','jpeg'],'image/png'=>['png'],'image/svg+xml'=>['svg'],'application/pdf'=>['pdf']];
|
||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
$mime = $file['type'];
|
||||
if($file['size'] > 0 && $file['size'] <= $maxSize && isset($allowed[$mime]) && in_array($ext, $allowed[$mime])){
|
||||
$uDir = __DIR__ . '/../../uploads/org_proofs';
|
||||
if(!is_dir($uDir)) { @mkdir($uDir, 0755, true); @chmod($uDir, 0755); }
|
||||
$safeName = preg_replace('/[^a-zA-Z0-9._-]/','_', basename($file['name']));
|
||||
$dst = $uDir . '/' . time() . '_' . bin2hex(random_bytes(6)) . '_' . $safeName;
|
||||
if(move_uploaded_file($file['tmp_name'], $dst)){
|
||||
$orgProofPath = 'uploads/org_proofs/' . basename($dst);
|
||||
try{ $up = $conn->prepare('UPDATE users SET org_proof_path = ? WHERE id = ?'); if($up){ $up->bind_param('si', $orgProofPath, $newId); $up->execute(); $up->close(); } }catch(Exception $e){}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// notify admins
|
||||
try{ send_bulk_admin_notification('New manager registration', "A new manager has registered: $email\nOrganization: $organization\nEmail already verified. Please review pending accounts.", $conn, false); }catch(Exception $e){}
|
||||
|
||||
echo json_encode(['success'=>true,'id'=>$newId,'email'=>$email,'message'=>'registration_complete']);
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
include __DIR__ . '/../../config/koneksi.php';
|
||||
include __DIR__ . '/../../config/auth.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$admin = require_bearer_login();
|
||||
if(!isset($admin['role']) || $admin['role'] !== 'admin'){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'forbidden']); exit; }
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
if(!$data || !isset($data['id'])){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_id']); exit; }
|
||||
$id = intval($data['id']);
|
||||
$reason = isset($data['reason']) ? $data['reason'] : null;
|
||||
|
||||
$st = $conn->prepare('UPDATE users SET status = ?, rejection_reason = ?, approved_by = ?, approved_at = NOW() WHERE id = ? AND role = "manager"');
|
||||
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
$status = 'rejected'; $adminId = intval($admin['id']); $st->bind_param('ssii', $status, $reason, $adminId, $id);
|
||||
$res = $st->execute(); $st->close();
|
||||
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
|
||||
echo json_encode(['success'=>true]);
|
||||
?>
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
include __DIR__ . '/../../config/koneksi.php';
|
||||
include __DIR__ . '/mail_helper.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Support JSON and form-encoded
|
||||
$data = null;
|
||||
if($_SERVER['REQUEST_METHOD'] === 'POST'){
|
||||
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||
if(strpos($contentType, 'application/json') !== false){
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
} else {
|
||||
$data = $_POST;
|
||||
}
|
||||
}
|
||||
|
||||
$email = isset($data['email']) ? strtolower(trim($data['email'])) : null;
|
||||
|
||||
if(!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)){
|
||||
http_response_code(400);
|
||||
echo json_encode(['success'=>false,'error'=>'invalid_email']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Generate 6-digit code
|
||||
$verificationCode = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
$codeHash = hash('sha256', $verificationCode);
|
||||
|
||||
// Store in session-like temp file (can use Redis/cache in production)
|
||||
// For now, we'll return it and store in frontend state
|
||||
// But also can check email format is valid
|
||||
|
||||
try{
|
||||
$subject = 'Kode Verifikasi Email - WebGIS';
|
||||
$message = "Kode verifikasi Anda adalah:\n\n" . $verificationCode . "\n\nKode ini berlaku selama 15 menit. Jangan bagikan kode ini kepada siapapun.\n\nJika Anda tidak melakukan pendaftaran ini, abaikan email ini.";
|
||||
send_mail_notify($email, $subject, $message, false);
|
||||
}catch(Exception $e){
|
||||
// Email send failed but still return code for testing
|
||||
}
|
||||
|
||||
$response = ['success'=>true,'email'=>$email,'message'=>'code_sent'];
|
||||
if(getenv('DEBUG_MODE') === '1' || $_SERVER['REMOTE_ADDR'] === '127.0.0.1'){
|
||||
$response['debug_code'] = $verificationCode;
|
||||
}
|
||||
echo json_encode($response);
|
||||
?>
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
// Simple geocoding proxy to Nominatim with basic file caching.
|
||||
// Usage: src/api/geocode.php?action=search&q=... OR src/api/geocode.php?action=reverse&lat=...&lon=...
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$action = isset($_GET['action']) ? $_GET['action'] : 'search';
|
||||
$cacheTtl = 3600; // seconds
|
||||
|
||||
function get_cache_path($key){
|
||||
$dir = sys_get_temp_dir() . '/webgis_geocode_cache';
|
||||
if(!is_dir($dir)) @mkdir($dir, 0755, true);
|
||||
return $dir . '/' . $key . '.json';
|
||||
}
|
||||
|
||||
function fetch_remote($url){
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'WebGIS/1.0 (+mailto:webgis@example.com)');
|
||||
$res = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if($res === false || !$res){
|
||||
return ['ok'=>false, 'error'=>$err ?: 'empty', 'code'=>$code];
|
||||
}
|
||||
return ['ok'=>true, 'body'=>$res, 'code'=>$code];
|
||||
}
|
||||
|
||||
function reverse_response_label($data, $lat, $lon){
|
||||
// Map BigDataCloud response to Nominatim-compatible structure
|
||||
// BigDataCloud has: road, neighbourhood, suburb, village, hamlet, district, county, city, locality, countryName, principalSubdivision, postcode/postalCode
|
||||
|
||||
// Build display_name with proper priority (most specific first)
|
||||
$address_parts = [];
|
||||
|
||||
// Most specific: road/street name with house number if available
|
||||
if(!empty($data['road'])){
|
||||
$address_parts[] = $data['road'];
|
||||
}
|
||||
|
||||
// Neighbourhood/area level
|
||||
if(!empty($data['neighbourhood']) && $data['neighbourhood'] !== ($data['road'] ?? '')){
|
||||
$address_parts[] = $data['neighbourhood'];
|
||||
}
|
||||
|
||||
// Suburb
|
||||
if(!empty($data['suburb']) && !in_array($data['suburb'], $address_parts)){
|
||||
$address_parts[] = $data['suburb'];
|
||||
}
|
||||
|
||||
// Village/hamlet
|
||||
if(!empty($data['village']) && !in_array($data['village'], $address_parts)){
|
||||
$address_parts[] = $data['village'];
|
||||
}
|
||||
if(!empty($data['hamlet']) && !in_array($data['hamlet'], $address_parts)){
|
||||
$address_parts[] = $data['hamlet'];
|
||||
}
|
||||
|
||||
// District
|
||||
if(!empty($data['district']) && !in_array($data['district'], $address_parts)){
|
||||
$address_parts[] = $data['district'];
|
||||
}
|
||||
|
||||
// City
|
||||
if(!empty($data['city']) && !in_array($data['city'], $address_parts)){
|
||||
$address_parts[] = $data['city'];
|
||||
}
|
||||
if(!empty($data['locality']) && $data['locality'] !== $data['city'] && !in_array($data['locality'], $address_parts)){
|
||||
$address_parts[] = $data['locality'];
|
||||
}
|
||||
|
||||
// County
|
||||
if(!empty($data['county']) && !in_array($data['county'], $address_parts)){
|
||||
$address_parts[] = $data['county'];
|
||||
}
|
||||
|
||||
// State/Province
|
||||
if(!empty($data['principalSubdivision']) && !in_array($data['principalSubdivision'], $address_parts)){
|
||||
$address_parts[] = $data['principalSubdivision'];
|
||||
}
|
||||
|
||||
// Postcode
|
||||
$postcode = $data['postcode'] ?? ($data['postalCode'] ?? null);
|
||||
if(!empty($postcode) && !in_array($postcode, $address_parts)){
|
||||
$address_parts[] = $postcode;
|
||||
}
|
||||
|
||||
// Country
|
||||
if(!empty($data['countryName']) && !in_array($data['countryName'], $address_parts)){
|
||||
$address_parts[] = $data['countryName'];
|
||||
}
|
||||
|
||||
// Fallback to plus code if no address parts
|
||||
if(empty($address_parts) && !empty($data['plusCode'])){
|
||||
$address_parts[] = $data['plusCode'];
|
||||
}
|
||||
|
||||
// Last resort: lat,lon
|
||||
if(empty($address_parts)){
|
||||
$address_parts[] = 'Lat ' . $lat . ', Lon ' . $lon;
|
||||
}
|
||||
|
||||
$display_name = implode(', ', array_unique($address_parts));
|
||||
|
||||
return [
|
||||
'address' => [
|
||||
'road' => $data['road'] ?? null,
|
||||
'neighbourhood' => $data['neighbourhood'] ?? null,
|
||||
'suburb' => $data['suburb'] ?? null,
|
||||
'village' => $data['village'] ?? null,
|
||||
'hamlet' => $data['hamlet'] ?? null,
|
||||
'district' => $data['district'] ?? null,
|
||||
'county' => $data['county'] ?? null,
|
||||
'city' => $data['city'] ?? null,
|
||||
'locality' => $data['locality'] ?? null,
|
||||
'state' => $data['principalSubdivision'] ?? null,
|
||||
'postcode' => $postcode,
|
||||
'country' => $data['countryName'] ?? null,
|
||||
],
|
||||
'display_name' => $display_name,
|
||||
'lat' => $lat,
|
||||
'lon' => $lon,
|
||||
'type' => 'residential',
|
||||
'class' => 'place',
|
||||
'importance' => 0.5,
|
||||
'provider' => 'bigdatacloud'
|
||||
];
|
||||
}
|
||||
|
||||
if($action === 'reverse'){
|
||||
$lat = isset($_GET['lat']) ? $_GET['lat'] : null;
|
||||
$lon = isset($_GET['lon']) ? $_GET['lon'] : null;
|
||||
if($lat === null || $lon === null){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_lat_lon']); exit; }
|
||||
$url = 'https://nominatim.openstreetmap.org/reverse?format=json&addressdetails=1&namedetails=1&zoom=18&lat=' . urlencode($lat) . '&lon=' . urlencode($lon) . '&email=webgis@example.com';
|
||||
$cacheKey = 'rev_' . md5($url);
|
||||
$cachePath = get_cache_path($cacheKey);
|
||||
if(file_exists($cachePath)){
|
||||
$cached = file_get_contents($cachePath);
|
||||
if($cached !== false && stripos($cached, 'Access denied') === false){
|
||||
echo $cached;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$res = fetch_remote($url);
|
||||
$body = null;
|
||||
// Nominatim is primary service. Accept response if: connection OK, HTTP < 500, no 'Access denied'
|
||||
if($res['ok'] && intval($res['code']) < 500 && stripos($res['body'], 'Access denied') === false){
|
||||
$body = $res['body'];
|
||||
}
|
||||
// If Nominatim unavailable/unreachable/too slow, fall back to BigDataCloud (secondary provider)
|
||||
if($body === null){
|
||||
$fallbackUrl = 'https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=' . urlencode($lat) . '&longitude=' . urlencode($lon) . '&localityLanguage=id';
|
||||
$fallbackRes = fetch_remote($fallbackUrl);
|
||||
if(!$fallbackRes['ok']){ http_response_code(503); echo json_encode(['success'=>false,'error'=>'remote_error','detail'=>$fallbackRes['error']]); exit; }
|
||||
if(intval($fallbackRes['code']) >= 500){ http_response_code(503); echo json_encode(['success'=>false,'error'=>'remote_5xx','code'=>$fallbackRes['code']]); exit; }
|
||||
$decoded = json_decode($fallbackRes['body'], true);
|
||||
if(!$decoded){ http_response_code(503); echo json_encode(['success'=>false,'error'=>'invalid_fallback_response']); exit; }
|
||||
$payload = reverse_response_label($decoded, $lat, $lon);
|
||||
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
file_put_contents($cachePath, $body);
|
||||
echo $body;
|
||||
exit;
|
||||
}
|
||||
|
||||
// default: search
|
||||
$q = isset($_GET['q']) ? trim($_GET['q']) : '';
|
||||
if($q === ''){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_q']); exit; }
|
||||
$url = 'https://nominatim.openstreetmap.org/search?format=json&addressdetails=1&limit=6&q=' . urlencode($q) . '&email=webgis@example.com';
|
||||
$cacheKey = 'search_' . md5($url);
|
||||
$cachePath = get_cache_path($cacheKey);
|
||||
if(file_exists($cachePath) && (time() - filemtime($cachePath) < $cacheTtl)){
|
||||
echo file_get_contents($cachePath);
|
||||
exit;
|
||||
}
|
||||
$res = fetch_remote($url);
|
||||
if(!$res['ok']){ http_response_code(503); echo json_encode(['success'=>false,'error'=>'remote_error','detail'=>$res['error']]); exit; }
|
||||
if(intval($res['code']) >= 500){ http_response_code(503); echo json_encode(['success'=>false,'error'=>'remote_5xx','code'=>$res['code']]); exit; }
|
||||
file_put_contents($cachePath, $res['body']);
|
||||
echo $res['body'];
|
||||
exit;
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// Return minimal client config (keep API keys out of the browser)
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
include __DIR__ . '/../config/config.php';
|
||||
|
||||
|
||||
echo json_encode(['api_key'=>null]);
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
include __DIR__ . '/../config/koneksi.php';
|
||||
include __DIR__ . '/../config/auth.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
function table_exists_local($conn, $tableName){
|
||||
$st = $conn->prepare("SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? LIMIT 1");
|
||||
if(!$st){ return false; }
|
||||
$st->bind_param('s', $tableName);
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$exists = $r && $r->fetch_assoc();
|
||||
$st->close();
|
||||
return (bool)$exists;
|
||||
}
|
||||
|
||||
// auto-clear assisted flag and current last_assisted_at after 30 days
|
||||
try{
|
||||
$conn->query("UPDATE lokasi SET assisted = 0, last_assisted_at = NULL, sumber_bantuan = NULL, bentuk_bantuan = NULL, assistance_notes = NULL WHERE assisted = 1 AND last_assisted_at IS NOT NULL AND last_assisted_at <= DATE_SUB(NOW(), INTERVAL 30 DAY)");
|
||||
}catch(Exception $e){}
|
||||
|
||||
// determine current user (may be null for guests)
|
||||
$currentUser = get_current_user_info();
|
||||
|
||||
$hasBantuanDetail = table_exists_local($conn, 'bantuan_detail');
|
||||
|
||||
$sql = $hasBantuanDetail
|
||||
? "SELECT l.*, COALESCE(a.total_bantuan, 0) AS total_bantuan, COALESCE(a.bantuan_bulan_ini, 0) AS bantuan_bulan_ini,
|
||||
p.nama AS sumber_bantuan_name, p.jenis AS sumber_bantuan_jenis
|
||||
FROM lokasi l
|
||||
LEFT JOIN (
|
||||
SELECT pemberi_lokasi_id,
|
||||
COUNT(*) AS total_bantuan,
|
||||
SUM(CASE
|
||||
WHEN tanggal_bantuan >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
|
||||
AND tanggal_bantuan < DATE_ADD(DATE_FORMAT(CURDATE(), '%Y-%m-01'), INTERVAL 1 MONTH)
|
||||
THEN 1 ELSE 0 END) AS bantuan_bulan_ini
|
||||
FROM bantuan_detail
|
||||
WHERE pemberi_lokasi_id IS NOT NULL
|
||||
GROUP BY pemberi_lokasi_id
|
||||
) a ON a.pemberi_lokasi_id = l.id
|
||||
LEFT JOIN lokasi p ON p.id = l.sumber_bantuan
|
||||
ORDER BY l.id ASC"
|
||||
: "SELECT l.*, 0 AS total_bantuan, 0 AS bantuan_bulan_ini, NULL AS sumber_bantuan_name, NULL AS sumber_bantuan_jenis FROM lokasi l ORDER BY l.id ASC";
|
||||
|
||||
try{
|
||||
$result = $conn->query($sql);
|
||||
}catch(Throwable $e){
|
||||
$result = false;
|
||||
}
|
||||
|
||||
if(!$result){
|
||||
$fallback = $conn->query("SELECT l.*, 0 AS total_bantuan, 0 AS bantuan_bulan_ini FROM lokasi l ORDER BY l.id ASC");
|
||||
if(!$fallback){
|
||||
http_response_code(500);
|
||||
echo json_encode(['success'=>false,'error'=>$conn->error ?: 'query_failed']);
|
||||
exit;
|
||||
}
|
||||
$result = $fallback;
|
||||
}
|
||||
|
||||
// apply masking rules: guests and non-owner managers see limited fields
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$owner = isset($row['created_by']) ? intval($row['created_by']) : null;
|
||||
$isAdmin = ($currentUser && isset($currentUser['role']) && $currentUser['role'] === 'admin');
|
||||
// Treat admin as manager-equivalent so admin can view manager-only fields
|
||||
$isManager = ($currentUser && isset($currentUser['role']) && ($currentUser['role'] === 'manager' || $currentUser['role'] === 'admin'));
|
||||
$isOwner = ($currentUser && isset($currentUser['id']) && intval($currentUser['id']) === $owner);
|
||||
if(!$isAdmin && !$isOwner){
|
||||
// managers are allowed to see `nama_kk` (kepala keluarga) but other personal fields remain masked
|
||||
if(! $isManager){ if(isset($row['nama_kk'])) $row['nama_kk'] = null; }
|
||||
if(isset($row['no_telp'])) $row['no_telp'] = null;
|
||||
if(isset($row['assistance_notes'])) $row['assistance_notes'] = null;
|
||||
}
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
include __DIR__ . '/../config/koneksi.php';
|
||||
include __DIR__ . '/../config/auth.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Require bearer/JWT auth only for deletes; managers may only delete their own records
|
||||
$currentUser = require_bearer_login();
|
||||
|
||||
// accept DELETE or POST with JSON body or form
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = null;
|
||||
if($_SERVER['REQUEST_METHOD'] === 'DELETE'){
|
||||
$id = isset($input['id']) ? $input['id'] : null;
|
||||
} else {
|
||||
$id = isset($_POST['id']) ? $_POST['id'] : (isset($input['id']) ? $input['id'] : null);
|
||||
}
|
||||
|
||||
function lokasi_id_uses_auto_increment($conn){
|
||||
$st = $conn->prepare("SELECT DATA_TYPE, EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lokasi' AND COLUMN_NAME = 'id' LIMIT 1");
|
||||
if(!$st){ return false; }
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$row = $r ? $r->fetch_assoc() : null;
|
||||
$st->close();
|
||||
if(!$row){ return false; }
|
||||
$dataType = strtolower((string)$row['DATA_TYPE']);
|
||||
$extra = strtolower((string)$row['EXTRA']);
|
||||
return strpos($extra, 'auto_increment') !== false || in_array($dataType, array('tinyint','smallint','mediumint','int','bigint'), true);
|
||||
}
|
||||
|
||||
$usesAutoId = lokasi_id_uses_auto_increment($conn);
|
||||
$id = trim((string)$id);
|
||||
if($usesAutoId){
|
||||
if(!ctype_digit($id)){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_id']); exit; }
|
||||
$id = intval($id);
|
||||
} elseif($id === ''){
|
||||
http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Enforce manager ownership for delete
|
||||
try{
|
||||
if(isset($currentUser['role']) && $currentUser['role'] === 'manager'){
|
||||
$owner = null;
|
||||
$selOwner = $conn->prepare('SELECT created_by FROM lokasi WHERE id = ? LIMIT 1');
|
||||
if($selOwner){
|
||||
if($usesAutoId){ $selOwner->bind_param('i', $id); } else { $selOwner->bind_param('s', $id); }
|
||||
$selOwner->execute();
|
||||
$rOwner = $selOwner->get_result();
|
||||
$rowOwner = $rOwner ? $rOwner->fetch_assoc() : null;
|
||||
$selOwner->close();
|
||||
$owner = $rowOwner && isset($rowOwner['created_by']) ? intval($rowOwner['created_by']) : null;
|
||||
}
|
||||
if($owner !== intval($currentUser['id'])){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'forbidden']); exit; }
|
||||
}
|
||||
}catch(Exception $e){ /* ignore */ }
|
||||
|
||||
$photoPath = null;
|
||||
$sel = $conn->prepare('SELECT photo_path FROM lokasi WHERE id = ? LIMIT 1');
|
||||
if($sel){
|
||||
$sel->bind_param($usesAutoId ? 'i' : 's', $id);
|
||||
$sel->execute();
|
||||
$r = $sel->get_result();
|
||||
if($row = $r->fetch_assoc()){
|
||||
$photoPath = isset($row['photo_path']) ? $row['photo_path'] : null;
|
||||
}
|
||||
$sel->close();
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare('DELETE FROM lokasi WHERE id = ?');
|
||||
if(!$stmt){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
$stmt->bind_param($usesAutoId ? 'i' : 's', $id);
|
||||
$res = $stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
if($res){
|
||||
if($photoPath){
|
||||
$root = realpath(__DIR__ . '/../../');
|
||||
$uploadDir = $root ? ($root . '/uploads') : (__DIR__ . '/../../uploads');
|
||||
$oldBase = basename(parse_url($photoPath, PHP_URL_PATH));
|
||||
$possibleFiles = array(
|
||||
$uploadDir . '/' . $oldBase,
|
||||
$uploadDir . '/' . $photoPath,
|
||||
$root ? ($root . '/' . ltrim($photoPath, '/')) : null
|
||||
);
|
||||
foreach($possibleFiles as $candidate){
|
||||
if(!$candidate) continue;
|
||||
if(file_exists($candidate) && is_file($candidate)){
|
||||
@unlink($candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo json_encode(['success'=>true,'message'=>'hapus']);
|
||||
}
|
||||
else { http_response_code(500); echo json_encode(['success'=>false,'error'=>'delete_failed']); }
|
||||
?>
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
include __DIR__ . '/../config/koneksi.php';
|
||||
include __DIR__ . '/../config/auth.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Require bearer/JWT auth only for creation
|
||||
$currentUser = require_bearer_login();
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if(!$data){
|
||||
http_response_code(400);
|
||||
echo json_encode(["success"=>false,"error"=>"invalid_json"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$nama = isset($data['nama']) ? $data['nama'] : '';
|
||||
$jenis = isset($data['jenis']) ? $data['jenis'] : '';
|
||||
$no_telp = isset($data['no_telp']) ? $data['no_telp'] : '';
|
||||
$buka = isset($data['buka_24_jam']) ? intval($data['buka_24_jam']) : 0;
|
||||
$alamat = isset($data['alamat']) ? $data['alamat'] : '';
|
||||
$lat = isset($data['latitude']) ? floatval($data['latitude']) : 0.0;
|
||||
$lng = isset($data['longitude']) ? floatval($data['longitude']) : 0.0;
|
||||
$nama_kk = isset($data['nama_kk']) ? $data['nama_kk'] : '';
|
||||
$rumah_status = isset($data['rumah_status']) ? $data['rumah_status'] : '';
|
||||
$members = isset($data['members']) ? intval($data['members']) : 0;
|
||||
$monthly_income = isset($data['monthly_income']) ? (int) round(floatval($data['monthly_income'])) : 0;
|
||||
$assisted = isset($data['assisted']) ? intval($data['assisted']) : 0;
|
||||
$last_assisted_at = isset($data['last_assisted_at']) ? $data['last_assisted_at'] : null;
|
||||
$sumber_bantuan = isset($data['sumber_bantuan']) ? $data['sumber_bantuan'] : null;
|
||||
$bentuk_bantuan = isset($data['bentuk_bantuan']) ? $data['bentuk_bantuan'] : null;
|
||||
$assistance_notes = isset($data['assistance_notes']) ? $data['assistance_notes'] : null;
|
||||
|
||||
function normalize_source_bantuan_id($conn, $value){
|
||||
if($value === null){ return null; }
|
||||
$raw = trim((string)$value);
|
||||
if($raw === ''){ return null; }
|
||||
if(ctype_digit($raw)){
|
||||
$id = intval($raw);
|
||||
$st = $conn->prepare('SELECT id FROM lokasi WHERE id = ? LIMIT 1');
|
||||
if(!$st){ return null; }
|
||||
$st->bind_param('i', $id);
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$found = $r && $r->fetch_assoc();
|
||||
$st->close();
|
||||
return $found ? $id : null;
|
||||
}
|
||||
return $raw === '' ? null : $raw;
|
||||
}
|
||||
|
||||
function lokasi_id_uses_auto_increment($conn){
|
||||
$st = $conn->prepare("SELECT DATA_TYPE, EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lokasi' AND COLUMN_NAME = 'id' LIMIT 1");
|
||||
if(!$st){ return false; }
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$row = $r ? $r->fetch_assoc() : null;
|
||||
$st->close();
|
||||
if(!$row){ return false; }
|
||||
$dataType = strtolower((string)$row['DATA_TYPE']);
|
||||
$extra = strtolower((string)$row['EXTRA']);
|
||||
return strpos($extra, 'auto_increment') !== false || in_array($dataType, array('tinyint','smallint','mediumint','int','bigint'), true);
|
||||
}
|
||||
|
||||
function generate_lokasi_string_id($conn){
|
||||
for($i = 0; $i < 20; $i++){
|
||||
$candidate = substr(bin2hex(random_bytes(4)), 0, 8);
|
||||
$st = $conn->prepare('SELECT id FROM lokasi WHERE id = ? LIMIT 1');
|
||||
if(!$st){ return $candidate; }
|
||||
$st->bind_param('s', $candidate);
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$found = $r && $r->fetch_assoc();
|
||||
$st->close();
|
||||
if(!$found){ return $candidate; }
|
||||
}
|
||||
return substr(bin2hex(random_bytes(4)), 0, 8);
|
||||
}
|
||||
|
||||
// ensure assistance-related columns exist (best-effort)
|
||||
try{ $conn->query("ALTER TABLE lokasi ADD COLUMN sumber_bantuan INT NULL"); }catch(Exception $e){}
|
||||
try{ $conn->query("ALTER TABLE lokasi ADD COLUMN bentuk_bantuan VARCHAR(100) NULL"); }catch(Exception $e){}
|
||||
try{ $conn->query("ALTER TABLE lokasi ADD COLUMN created_by INT NULL"); }catch(Exception $e){}
|
||||
|
||||
$created_by = isset($currentUser['id']) ? intval($currentUser['id']) : null;
|
||||
|
||||
$sumber_bantuan = normalize_source_bantuan_id($conn, $sumber_bantuan);
|
||||
|
||||
$usesAutoId = lokasi_id_uses_auto_increment($conn);
|
||||
|
||||
$insertValues = array($nama, $jenis, $no_telp, $buka, $alamat, $lat, $lng, $nama_kk, $rumah_status, $members, $monthly_income, $assisted, $last_assisted_at, $sumber_bantuan, $bentuk_bantuan, $assistance_notes);
|
||||
|
||||
// include created_by field in insert
|
||||
if($usesAutoId){
|
||||
$params = array_merge($insertValues, array($created_by));
|
||||
$types = str_repeat('s', count($params));
|
||||
$stmt = $conn->prepare("INSERT INTO lokasi (nama, jenis, no_telp, buka_24_jam, alamat, latitude, longitude, nama_kk, rumah_status, members, monthly_income, assisted, last_assisted_at, sumber_bantuan, bentuk_bantuan, assistance_notes, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
if(!$stmt){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
$bind_names[] = $types;
|
||||
for($i=0;$i<count($params);$i++){ $bind_name = 'bind'.$i; $$bind_name = $params[$i]; $bind_names[] = &$$bind_name; }
|
||||
call_user_func_array(array($stmt,'bind_param'), $bind_names);
|
||||
$res = $stmt->execute();
|
||||
$newId = $conn->insert_id;
|
||||
$stmt->close();
|
||||
} else {
|
||||
$newId = generate_lokasi_string_id($conn);
|
||||
$params = array_merge(array($newId), $insertValues, array($created_by));
|
||||
$types = str_repeat('s', count($params));
|
||||
$stmt = $conn->prepare("INSERT INTO lokasi (id, nama, jenis, no_telp, buka_24_jam, alamat, latitude, longitude, nama_kk, rumah_status, members, monthly_income, assisted, last_assisted_at, sumber_bantuan, bentuk_bantuan, assistance_notes, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
if(!$stmt){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
$bind_names = array($types);
|
||||
for($i=0;$i<count($params);$i++){ $bind_name = 'b'. $i; $$bind_name = $params[$i]; $bind_names[] = &$$bind_name; }
|
||||
call_user_func_array(array($stmt,'bind_param'), $bind_names);
|
||||
$res = $stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
if(!$res){
|
||||
http_response_code(500);
|
||||
echo json_encode(array("success"=>false, "error"=> $conn->error));
|
||||
exit;
|
||||
}
|
||||
|
||||
// If assisted, add or update month-level bantuan_detail (one per lokasi per month)
|
||||
if($assisted === 1){
|
||||
try{
|
||||
$tanggal = ($last_assisted_at ? $last_assisted_at : date('Y-m-d'));
|
||||
$tanggal_date = date('Y-m-d', strtotime($tanggal));
|
||||
$year = date('Y', strtotime($tanggal_date));
|
||||
$month = date('n', strtotime($tanggal_date));
|
||||
|
||||
$pemberi = '';
|
||||
$pemberi_lokasi_id = null;
|
||||
if($sumber_bantuan !== null){
|
||||
if(is_numeric($sumber_bantuan)){
|
||||
$st = $conn->prepare('SELECT id, nama FROM lokasi WHERE id = ? LIMIT 1');
|
||||
if($st){
|
||||
$sid = intval($sumber_bantuan);
|
||||
$st->bind_param('i', $sid);
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
if($r && ($row = $r->fetch_assoc())){ $pemberi = $row['nama']; $pemberi_lokasi_id = intval($row['id']); }
|
||||
$st->close();
|
||||
}
|
||||
} else {
|
||||
$pemberi = (string)$sumber_bantuan;
|
||||
}
|
||||
}
|
||||
|
||||
$chk = $conn->prepare('SELECT id, jumlah FROM bantuan_detail WHERE lokasi_id = ? AND YEAR(tanggal_bantuan) = ? AND MONTH(tanggal_bantuan) = ? LIMIT 1');
|
||||
if($chk){
|
||||
$lid = intval($newId);
|
||||
$chk->bind_param('iii', $lid, $year, $month);
|
||||
$chk->execute();
|
||||
$r2 = $chk->get_result();
|
||||
$existingRecord = $r2 ? $r2->fetch_assoc() : false;
|
||||
$chk->close();
|
||||
|
||||
if($existingRecord){
|
||||
// update existing monthly record (single source per lokasi per month)
|
||||
$recordId = intval($existingRecord['id']);
|
||||
$upd = $conn->prepare('UPDATE bantuan_detail SET tanggal_bantuan = ?, pemberi_bantuan = ?, catatan = ?, pemberi_lokasi_id = ?, jumlah = 1 WHERE id = ?');
|
||||
if($upd){
|
||||
$p_lokasi = $pemberi_lokasi_id !== null ? $pemberi_lokasi_id : null;
|
||||
$upd->bind_param('sssii', $tanggal_date, $pemberi, $assistance_notes, $p_lokasi, $recordId);
|
||||
@ $upd->execute();
|
||||
$upd->close();
|
||||
}
|
||||
} else {
|
||||
// insert new monthly record
|
||||
if($pemberi_lokasi_id !== null){
|
||||
$ins = $conn->prepare('INSERT INTO bantuan_detail (lokasi_id, tanggal_bantuan, pemberi_bantuan, catatan, pemberi_lokasi_id, jumlah) VALUES (?, ?, ?, ?, ?, 1)');
|
||||
if($ins){
|
||||
$ins->bind_param('isssi', $lid, $tanggal_date, $pemberi, $assistance_notes, $pemberi_lokasi_id);
|
||||
@ $ins->execute();
|
||||
$ins->close();
|
||||
}
|
||||
} else {
|
||||
$ins = $conn->prepare('INSERT INTO bantuan_detail (lokasi_id, tanggal_bantuan, pemberi_bantuan, catatan, jumlah) VALUES (?, ?, ?, ?, 1)');
|
||||
if($ins){
|
||||
$ins->bind_param('isss', $lid, $tanggal_date, $pemberi, $assistance_notes);
|
||||
@ $ins->execute();
|
||||
$ins->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// ignore insertion errors for now
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(array("success"=>true, "id"=>$newId));
|
||||
?>
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
include __DIR__ . '/../config/koneksi.php';
|
||||
include __DIR__ . '/../config/auth.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Require bearer/JWT auth only for edits
|
||||
$currentUser = require_bearer_login();
|
||||
|
||||
// ensure created_by exists
|
||||
try{ $conn->query("ALTER TABLE lokasi ADD COLUMN IF NOT EXISTS created_by INT NULL"); }catch(Exception $e){}
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
$id = isset($data['id']) ? $data['id'] : '';
|
||||
$id = trim((string)$id);
|
||||
$nama = isset($data['nama']) ? $data['nama'] : null;
|
||||
$no_telp = isset($data['no_telp']) ? $data['no_telp'] : null;
|
||||
$buka = isset($data['buka_24_jam']) ? intval($data['buka_24_jam']) : null;
|
||||
$alamat = isset($data['alamat']) ? $data['alamat'] : null;
|
||||
$jenis = isset($data['jenis']) ? $data['jenis'] : null;
|
||||
|
||||
// household fields
|
||||
$nama_kk = isset($data['nama_kk']) ? $data['nama_kk'] : null;
|
||||
$building_type = isset($data['building_type']) ? $data['building_type'] : null;
|
||||
$rumah_status = isset($data['rumah_status']) ? $data['rumah_status'] : null;
|
||||
$members = isset($data['members']) ? (is_numeric($data['members']) ? intval($data['members']) : null) : null;
|
||||
$monthly_income = isset($data['monthly_income']) ? (is_numeric($data['monthly_income']) ? (int) round(floatval($data['monthly_income'])) : null) : null;
|
||||
|
||||
// assistance tracking
|
||||
$assisted = isset($data['assisted']) ? (intval($data['assisted']) ? 1 : 0) : null;
|
||||
$last_assisted_at = isset($data['last_assisted_at']) ? $data['last_assisted_at'] : null;
|
||||
$sumber_bantuan = isset($data['sumber_bantuan']) ? $data['sumber_bantuan'] : null;
|
||||
$bentuk_bantuan = isset($data['bentuk_bantuan']) ? $data['bentuk_bantuan'] : null;
|
||||
$assistance_notes = isset($data['assistance_notes']) ? $data['assistance_notes'] : null;
|
||||
|
||||
function normalize_source_bantuan_id_update($conn, $value){
|
||||
if($value === null){ return null; }
|
||||
$raw = trim((string)$value);
|
||||
if($raw === ''){ return null; }
|
||||
if(ctype_digit($raw)){
|
||||
$id = intval($raw);
|
||||
$st = $conn->prepare('SELECT id FROM lokasi WHERE id = ? LIMIT 1');
|
||||
if(!$st){ return null; }
|
||||
$st->bind_param('i', $id);
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$found = $r && $r->fetch_assoc();
|
||||
$st->close();
|
||||
return $found ? $id : null;
|
||||
}
|
||||
$st = $conn->prepare('SELECT id FROM lokasi WHERE LOWER(nama) = LOWER(?) LIMIT 1');
|
||||
if(!$st){ return null; }
|
||||
$st->bind_param('s', $raw);
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$row = $r ? $r->fetch_assoc() : null;
|
||||
$st->close();
|
||||
return $row ? intval($row['id']) : null;
|
||||
}
|
||||
|
||||
function lokasi_id_uses_auto_increment($conn){
|
||||
$st = $conn->prepare("SELECT DATA_TYPE, EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lokasi' AND COLUMN_NAME = 'id' LIMIT 1");
|
||||
if(!$st){ return false; }
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$row = $r ? $r->fetch_assoc() : null;
|
||||
$st->close();
|
||||
if(!$row){ return false; }
|
||||
$dataType = strtolower((string)$row['DATA_TYPE']);
|
||||
$extra = strtolower((string)$row['EXTRA']);
|
||||
return strpos($extra, 'auto_increment') !== false || in_array($dataType, array('tinyint','smallint','mediumint','int','bigint'), true);
|
||||
}
|
||||
|
||||
$sumber_bantuan = normalize_source_bantuan_id_update($conn, $sumber_bantuan);
|
||||
$usesAutoId = lokasi_id_uses_auto_increment($conn);
|
||||
|
||||
$sets = [];
|
||||
$types = '';
|
||||
$values = [];
|
||||
if($nama !== null){ $sets[] = 'nama = ?'; $types .= 's'; $values[] = $nama; }
|
||||
if($no_telp !== null){ $sets[] = 'no_telp = ?'; $types .= 's'; $values[] = $no_telp; }
|
||||
if($buka !== null){ $sets[] = 'buka_24_jam = ?'; $types .= 'i'; $values[] = $buka; }
|
||||
if($alamat !== null){ $sets[] = 'alamat = ?'; $types .= 's'; $values[] = $alamat; }
|
||||
if($jenis !== null){ $sets[] = 'jenis = ?'; $types .= 's'; $values[] = $jenis; }
|
||||
|
||||
if($nama_kk !== null){ $sets[] = 'nama_kk = ?'; $types .= 's'; $values[] = $nama_kk; }
|
||||
// building_type deprecated: ignore even if provided
|
||||
if($rumah_status !== null){ $sets[] = 'rumah_status = ?'; $types .= 's'; $values[] = $rumah_status; }
|
||||
if($members !== null){ $sets[] = 'members = ?'; $types .= 'i'; $values[] = $members; }
|
||||
if($monthly_income !== null){ $sets[] = 'monthly_income = ?'; $types .= 'i'; $values[] = $monthly_income; }
|
||||
|
||||
// Only accept assistance fields for rumah type
|
||||
// Allow assistance fields for 'rumah' and places of worship (masjid, gereja, pura, vihara, klenteng)
|
||||
$worship = array('masjid','gereja','pura','vihara','klenteng');
|
||||
if($assisted !== null && ($jenis === null || $jenis === 'rumah' || in_array($jenis, $worship))){ $sets[] = 'assisted = ?'; $types .= 'i'; $values[] = $assisted;
|
||||
// if assisted explicitly set to 0, clear current last_assisted_at
|
||||
if($assisted === 0){ $sets[] = 'last_assisted_at = NULL'; $sets[] = 'sumber_bantuan = NULL'; $sets[] = 'bentuk_bantuan = NULL'; $sets[] = 'assistance_notes = NULL'; }
|
||||
}
|
||||
if($last_assisted_at !== null && ($jenis === null || $jenis === 'rumah' || in_array($jenis, $worship))){ $sets[] = 'last_assisted_at = ?'; $types .= 's'; $values[] = $last_assisted_at; }
|
||||
if($sumber_bantuan !== null && ($jenis === null || $jenis === 'rumah' || in_array($jenis, $worship))){ $sets[] = 'sumber_bantuan = ?'; $types .= 'i'; $values[] = $sumber_bantuan; }
|
||||
if($bentuk_bantuan !== null && ($jenis === null || $jenis === 'rumah' || in_array($jenis, $worship))){ $sets[] = 'bentuk_bantuan = ?'; $types .= 's'; $values[] = $bentuk_bantuan; }
|
||||
if($assistance_notes !== null && ($jenis === null || $jenis === 'rumah' || in_array($jenis, $worship))){ $sets[] = 'assistance_notes = ?'; $types .= 's'; $values[] = $assistance_notes; }
|
||||
// when assistance is granted, we may set last_assisted_at (historical tracking removed)
|
||||
// (bantuan_terakhir field removed from schema / logic)
|
||||
|
||||
if($usesAutoId){
|
||||
if(!ctype_digit($id)){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_id']); exit; }
|
||||
$id = intval($id);
|
||||
} elseif($id === ''){
|
||||
http_response_code(400);
|
||||
echo json_encode(['success'=>false,'error'=>'invalid_id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Enforce manager edit rules: managers may fully edit their own lokasi.
|
||||
// Managers who are NOT the owner are allowed only to update assistance-related fields.
|
||||
try{
|
||||
if(isset($currentUser['role']) && $currentUser['role'] === 'manager'){
|
||||
$sel = $conn->prepare('SELECT created_by FROM lokasi WHERE id = ? LIMIT 1');
|
||||
if($sel){
|
||||
if($usesAutoId){ $sel->bind_param('i', $id); } else { $sel->bind_param('s', $id); }
|
||||
$sel->execute(); $r = $sel->get_result(); $row = $r ? $r->fetch_assoc() : null; $sel->close();
|
||||
$owner = $row && isset($row['created_by']) ? intval($row['created_by']) : null;
|
||||
if($owner !== intval($currentUser['id'])){
|
||||
// user is a manager but not the owner. Allow only assistance-only updates.
|
||||
$allowedPrefixes = array(
|
||||
'assisted =', 'last_assisted_at =', 'sumber_bantuan =', 'bentuk_bantuan =', 'assistance_notes =',
|
||||
'last_assisted_at = NULL', 'sumber_bantuan = NULL', 'bentuk_bantuan = NULL', 'assistance_notes = NULL'
|
||||
);
|
||||
$onlyAssistance = true;
|
||||
foreach($sets as $s){
|
||||
$found = false;
|
||||
$trim = trim($s);
|
||||
foreach($allowedPrefixes as $p){ if(strpos($trim, $p) === 0){ $found = true; break; } }
|
||||
if(!$found){ $onlyAssistance = false; break; }
|
||||
}
|
||||
if(!$onlyAssistance){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'forbidden']); exit; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(Exception $e){ /* ignore */ }
|
||||
|
||||
if(empty($sets)){
|
||||
http_response_code(400);
|
||||
echo json_encode(['success'=>false,'error'=>'missing_id_or_fields']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = 'UPDATE lokasi SET ' . implode(', ', $sets) . ' WHERE id = ?';
|
||||
$types .= $usesAutoId ? 'i' : 's';
|
||||
$values[] = $id;
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
if(!$stmt){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
|
||||
// bind params dynamically
|
||||
$bind_names[] = $types;
|
||||
for ($i=0; $i<count($values); $i++){
|
||||
$bind_name = 'bind' . $i;
|
||||
$$bind_name = $values[$i];
|
||||
$bind_names[] = &$$bind_name;
|
||||
}
|
||||
call_user_func_array([$stmt, 'bind_param'], $bind_names);
|
||||
$res = $stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
if($res){
|
||||
// Jika update berhasil dan ada tanda bantuan, coba masukkan record ke bantuan_detail
|
||||
try {
|
||||
// If assisted explicitly reset to 0, delete existing bantuan_detail for this lokasi for the same month
|
||||
if($assisted !== null && $assisted === 0){
|
||||
$delYear = date('Y');
|
||||
$delMonth = date('n');
|
||||
try{
|
||||
$del = $conn->prepare('DELETE FROM bantuan_detail WHERE lokasi_id = ? AND YEAR(tanggal_bantuan) = ? AND MONTH(tanggal_bantuan) = ?');
|
||||
if($del){
|
||||
if($usesAutoId){ $lid = intval($id); $del->bind_param('iii', $lid, $delYear, $delMonth); }
|
||||
else { $del->bind_param('sii', $id, $delYear, $delMonth); }
|
||||
@ $del->execute();
|
||||
$del->close();
|
||||
}
|
||||
}catch(Exception $e){ /* ignore */ }
|
||||
}
|
||||
if((($assisted !== null && $assisted === 1) || $last_assisted_at !== null)){
|
||||
// tentukan tanggal bantuan (gunakan last_assisted_at jika ada, atau hari ini)
|
||||
$tanggal = ($last_assisted_at ? $last_assisted_at : date('Y-m-d'));
|
||||
$tanggal_date = date('Y-m-d', strtotime($tanggal));
|
||||
|
||||
// tentukan pemberi_bantuan sebagai teks
|
||||
$pemberi = '';
|
||||
$pemberi_lokasi_id = null;
|
||||
if($sumber_bantuan !== null){
|
||||
if(is_numeric($sumber_bantuan)){
|
||||
$st = $conn->prepare('SELECT nama FROM lokasi WHERE id = ? LIMIT 1');
|
||||
if($st){
|
||||
$sid = intval($sumber_bantuan);
|
||||
$st->bind_param('i', $sid);
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
if($r && ($row = $r->fetch_assoc())){ $pemberi = $row['nama']; $pemberi_lokasi_id = $sid; }
|
||||
$st->close();
|
||||
}
|
||||
} else {
|
||||
$pemberi = (string)$sumber_bantuan;
|
||||
}
|
||||
}
|
||||
|
||||
// Use month-level uniqueness: one bantuan record per lokasi per month
|
||||
$year = date('Y', strtotime($tanggal_date));
|
||||
$month = date('n', strtotime($tanggal_date));
|
||||
$chk = $conn->prepare('SELECT id, jumlah FROM bantuan_detail WHERE lokasi_id = ? AND YEAR(tanggal_bantuan) = ? AND MONTH(tanggal_bantuan) = ? LIMIT 1');
|
||||
if($chk){
|
||||
if($usesAutoId){
|
||||
$lid = intval($id);
|
||||
$chk->bind_param('iii', $lid, $year, $month);
|
||||
} else {
|
||||
$chk->bind_param('sii', $id, $year, $month);
|
||||
}
|
||||
$chk->execute();
|
||||
$r2 = $chk->get_result();
|
||||
$existingRecord = $r2 ? $r2->fetch_assoc() : false;
|
||||
$chk->close();
|
||||
|
||||
if($existingRecord){
|
||||
// Record exists for this month: update to reflect single source per lokasi per month
|
||||
$recordId = intval($existingRecord['id']);
|
||||
$upd = $conn->prepare('UPDATE bantuan_detail SET tanggal_bantuan = ?, pemberi_bantuan = ?, catatan = ?, pemberi_lokasi_id = ?, jumlah = 1 WHERE id = ?');
|
||||
if($upd){
|
||||
$p_lokasi = $pemberi_lokasi_id !== null ? $pemberi_lokasi_id : null;
|
||||
if($usesAutoId){
|
||||
$upd->bind_param('sssii', $tanggal_date, $pemberi, $assistance_notes, $p_lokasi, $recordId);
|
||||
} else {
|
||||
$upd->bind_param('sssii', $tanggal_date, $pemberi, $assistance_notes, $p_lokasi, $recordId);
|
||||
}
|
||||
@ $upd->execute();
|
||||
$upd->close();
|
||||
}
|
||||
} else {
|
||||
// Record doesn't exist: insert new with jumlah=1
|
||||
if($pemberi_lokasi_id !== null){
|
||||
$ins = $conn->prepare('INSERT INTO bantuan_detail (lokasi_id, tanggal_bantuan, pemberi_bantuan, catatan, pemberi_lokasi_id, jumlah) VALUES (?, ?, ?, ?, ?, 1)');
|
||||
if($ins){
|
||||
if($usesAutoId){
|
||||
$ins->bind_param('isssi', $lid, $tanggal_date, $pemberi, $assistance_notes, $pemberi_lokasi_id);
|
||||
} else {
|
||||
$ins->bind_param('isssi', $id, $tanggal_date, $pemberi, $assistance_notes, $pemberi_lokasi_id);
|
||||
}
|
||||
@ $ins->execute();
|
||||
$ins->close();
|
||||
}
|
||||
} else {
|
||||
$ins = $conn->prepare('INSERT INTO bantuan_detail (lokasi_id, tanggal_bantuan, pemberi_bantuan, catatan, jumlah) VALUES (?, ?, ?, ?, 1)');
|
||||
if($ins){
|
||||
if($usesAutoId){
|
||||
$ins->bind_param('isss', $lid, $tanggal_date, $pemberi, $assistance_notes);
|
||||
} else {
|
||||
$ins->bind_param('isss', $id, $tanggal_date, $pemberi, $assistance_notes);
|
||||
}
|
||||
@ $ins->execute();
|
||||
$ins->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log('Insert bantuan_detail failed during update: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
echo json_encode(["success"=>true]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(["success"=>false, "error"=>$conn->error]);
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
include __DIR__ . '/../config/koneksi.php';
|
||||
include __DIR__ . '/../config/auth.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$currentUser = require_bearer_login();
|
||||
|
||||
if(empty($_FILES['file']) || empty($_POST['id'])){
|
||||
http_response_code(400);
|
||||
echo json_encode(['success'=>false,'error'=>'missing_file_or_id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = $_POST['id'];
|
||||
$id = trim((string)$id);
|
||||
|
||||
function lokasi_id_uses_auto_increment($conn){
|
||||
$st = $conn->prepare("SELECT DATA_TYPE, EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lokasi' AND COLUMN_NAME = 'id' LIMIT 1");
|
||||
if(!$st){ return false; }
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$row = $r ? $r->fetch_assoc() : null;
|
||||
$st->close();
|
||||
if(!$row){ return false; }
|
||||
$dataType = strtolower((string)$row['DATA_TYPE']);
|
||||
$extra = strtolower((string)$row['EXTRA']);
|
||||
return strpos($extra, 'auto_increment') !== false || in_array($dataType, array('tinyint','smallint','mediumint','int','bigint'), true);
|
||||
}
|
||||
|
||||
$usesAutoId = lokasi_id_uses_auto_increment($conn);
|
||||
if($usesAutoId){
|
||||
if(!ctype_digit($id)){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_id']); exit; }
|
||||
$id = intval($id);
|
||||
} elseif($id === ''){
|
||||
http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Enforce manager ownership for uploads
|
||||
try{
|
||||
if(isset($currentUser['role']) && $currentUser['role'] === 'manager'){
|
||||
$owner = null;
|
||||
$selOwner = $conn->prepare('SELECT created_by FROM lokasi WHERE id = ? LIMIT 1');
|
||||
if($selOwner){
|
||||
if($usesAutoId){ $selOwner->bind_param('i', $id); } else { $selOwner->bind_param('s', $id); }
|
||||
$selOwner->execute();
|
||||
$rOwner = $selOwner->get_result();
|
||||
$rowOwner = $rOwner ? $rOwner->fetch_assoc() : null;
|
||||
$selOwner->close();
|
||||
$owner = $rowOwner && isset($rowOwner['created_by']) ? intval($rowOwner['created_by']) : null;
|
||||
}
|
||||
if($owner !== intval($currentUser['id'])){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'forbidden']); exit; }
|
||||
}
|
||||
}catch(Exception $e){ /* ignore */ }
|
||||
$file = $_FILES['file'];
|
||||
|
||||
if($file['error'] !== UPLOAD_ERR_OK){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'upload_error']); exit; }
|
||||
|
||||
// validate size (<=5MB)
|
||||
if($file['size'] > 5 * 1024 * 1024){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'file_too_large']); exit; }
|
||||
|
||||
// validate image type
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
if(strpos($mime,'image/') !== 0){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_image_type']); exit; }
|
||||
|
||||
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||
$safeName = preg_replace('/[^a-zA-Z0-9_-]/','_',basename($file['name']));
|
||||
// normalize uploads directory path and create if needed
|
||||
$root = realpath(__DIR__ . '/../../');
|
||||
$targetDir = $root ? ($root . '/uploads') : (__DIR__ . '/../../uploads');
|
||||
if(!is_dir($targetDir)){
|
||||
@mkdir($targetDir, 0777, true);
|
||||
}
|
||||
@chmod($targetDir, 0777);
|
||||
// ensure extension preserved
|
||||
$target = $targetDir . '/' . $id . '_' . time() . '_' . $safeName;
|
||||
|
||||
// ensure target directory is writable
|
||||
@chmod($targetDir, 0777);
|
||||
$tmp = $file['tmp_name'];
|
||||
if(!is_uploaded_file($tmp)){
|
||||
http_response_code(500);
|
||||
echo json_encode(['success'=>false,'error'=>'not_uploaded_file','tmp'=>$tmp,'targetDir'=>$targetDir,'resolved_root'=>$root]);
|
||||
exit;
|
||||
}
|
||||
// do not abort here; some filesystems (fuse/ntfs) report not-writable
|
||||
// we'll attempt move and fall back to PHP temp dir if it fails
|
||||
if(!move_uploaded_file($file['tmp_name'], $target)){
|
||||
// Do not attempt other fallbacks. Return an explicit error so the caller knows upload failed.
|
||||
$last = error_get_last();
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'move_failed',
|
||||
'tmp' => $tmp,
|
||||
'target' => $target,
|
||||
'is_uploaded' => is_uploaded_file($tmp),
|
||||
'target_writable' => is_writable($targetDir),
|
||||
'last_error' => $last,
|
||||
'resolved_root' => $root
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$webPath = 'uploads/' . basename($target);
|
||||
// fetch existing photo_path so we can remove it after successful update
|
||||
$old = null;
|
||||
$sel = $conn->prepare('SELECT photo_path FROM lokasi WHERE id = ?');
|
||||
if($sel){
|
||||
$sel->bind_param($usesAutoId ? 'i' : 's', $id);
|
||||
$sel->execute();
|
||||
$r = $sel->get_result();
|
||||
if($row = $r->fetch_assoc()) $old = $row['photo_path'];
|
||||
$sel->close();
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare('UPDATE lokasi SET photo_path = ? WHERE id = ?');
|
||||
if(!$stmt){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||
$stmt->bind_param($usesAutoId ? 'si' : 'ss', $webPath, $id);
|
||||
$res = $stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
if($res){
|
||||
// remove previous uploaded file if it was stored under uploads/
|
||||
if($old){
|
||||
// extract basename in case old contains query params
|
||||
$oldBase = basename(parse_url($old, PHP_URL_PATH));
|
||||
$oldFull = $targetDir . '/' . $oldBase;
|
||||
if(file_exists($oldFull) && is_file($oldFull)){
|
||||
@unlink($oldFull);
|
||||
}
|
||||
}
|
||||
echo json_encode(['success'=>true,'path'=>$webPath]);
|
||||
} else {
|
||||
http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]);
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
include_once __DIR__ . '/config.php';
|
||||
|
||||
if(session_status() === PHP_SESSION_NONE) session_start();
|
||||
|
||||
function start_session_if_needed(){ if(session_status() === PHP_SESSION_NONE) session_start(); }
|
||||
|
||||
function auth_headers(){
|
||||
if(function_exists('getallheaders')){
|
||||
$headers = getallheaders();
|
||||
if(is_array($headers)) return $headers;
|
||||
}
|
||||
$headers = [];
|
||||
foreach($_SERVER as $key => $value){
|
||||
if(strpos($key, 'HTTP_') === 0){
|
||||
$name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))));
|
||||
$headers[$name] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
function auth_header_value($name){
|
||||
$headers = auth_headers();
|
||||
foreach($headers as $key => $value){
|
||||
if(strtolower($key) === strtolower($name)) return $value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function auth_json_response($statusCode, $payload){
|
||||
http_response_code($statusCode);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($payload);
|
||||
exit;
|
||||
}
|
||||
|
||||
function base64url_encode($data){ return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); }
|
||||
function base64url_decode($data){ return base64_decode(strtr($data, '-_', '+/')); }
|
||||
|
||||
function jwt_issue($claims, $ttlSeconds = 86400){
|
||||
$now = time();
|
||||
$payload = array_merge([
|
||||
'iss' => 'webgis',
|
||||
'iat' => $now,
|
||||
'exp' => $now + max(60, intval($ttlSeconds)),
|
||||
], $claims);
|
||||
$header = ['alg' => 'HS256', 'typ' => 'JWT'];
|
||||
$encodedHeader = base64url_encode(json_encode($header));
|
||||
$encodedPayload = base64url_encode(json_encode($payload));
|
||||
$signature = hash_hmac('sha256', $encodedHeader . '.' . $encodedPayload, JWT_SECRET, true);
|
||||
return $encodedHeader . '.' . $encodedPayload . '.' . base64url_encode($signature);
|
||||
}
|
||||
|
||||
function jwt_verify($token){
|
||||
if(!$token || strpos($token, '.') === false) return null;
|
||||
$parts = explode('.', $token);
|
||||
if(count($parts) !== 3) return null;
|
||||
list($h, $p, $s) = $parts;
|
||||
$header = json_decode(base64url_decode($h), true);
|
||||
$payload = json_decode(base64url_decode($p), true);
|
||||
if(!$header || !$payload) return null;
|
||||
if(!isset($header['alg']) || $header['alg'] !== 'HS256') return null;
|
||||
$expected = base64url_encode(hash_hmac('sha256', $h . '.' . $p, JWT_SECRET, true));
|
||||
if(!hash_equals($expected, $s)) return null;
|
||||
if(isset($payload['exp']) && time() > intval($payload['exp'])) return null;
|
||||
return $payload;
|
||||
}
|
||||
|
||||
function auth_basic_credentials(){
|
||||
$header = auth_header_value('Authorization');
|
||||
if(!$header && isset($_SERVER['PHP_AUTH_USER'])){
|
||||
return [$_SERVER['PHP_AUTH_USER'], isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''];
|
||||
}
|
||||
if(!$header || stripos($header, 'Basic ') !== 0) return [null, null];
|
||||
$decoded = base64_decode(substr($header, 6));
|
||||
if($decoded === false || strpos($decoded, ':') === false) return [null, null];
|
||||
list($user, $pass) = explode(':', $decoded, 2);
|
||||
return [$user, $pass];
|
||||
}
|
||||
|
||||
function auth_internal_cipher(){
|
||||
return 'aes-256-gcm';
|
||||
}
|
||||
|
||||
function auth_internal_key(){
|
||||
return hash('sha256', INTERNAL_AUTH_KEY, true);
|
||||
}
|
||||
|
||||
function internal_auth_issue($claims, $ttlSeconds = 300){
|
||||
$payload = array_merge([
|
||||
'iss' => 'webgis-internal',
|
||||
'iat' => time(),
|
||||
'exp' => time() + max(30, intval($ttlSeconds)),
|
||||
'nonce' => bin2hex(random_bytes(8))
|
||||
], $claims);
|
||||
$plaintext = json_encode($payload);
|
||||
$iv = random_bytes(12);
|
||||
$tag = '';
|
||||
$ciphertext = openssl_encrypt($plaintext, auth_internal_cipher(), auth_internal_key(), OPENSSL_RAW_DATA, $iv, $tag);
|
||||
if($ciphertext === false) return null;
|
||||
return base64url_encode($iv . $tag . $ciphertext);
|
||||
}
|
||||
|
||||
function internal_auth_verify($token){
|
||||
if(!$token) return null;
|
||||
$raw = base64url_decode($token);
|
||||
if($raw === false || strlen($raw) < 28) return null;
|
||||
$iv = substr($raw, 0, 12);
|
||||
$tag = substr($raw, 12, 16);
|
||||
$ciphertext = substr($raw, 28);
|
||||
$plaintext = openssl_decrypt($ciphertext, auth_internal_cipher(), auth_internal_key(), OPENSSL_RAW_DATA, $iv, $tag);
|
||||
if($plaintext === false) return null;
|
||||
$payload = json_decode($plaintext, true);
|
||||
if(!$payload) return null;
|
||||
if(isset($payload['exp']) && time() > intval($payload['exp'])) return null;
|
||||
return $payload;
|
||||
}
|
||||
|
||||
function get_auth_context(){
|
||||
start_session_if_needed();
|
||||
global $conn;
|
||||
$context = ['user' => null, 'mode' => null];
|
||||
|
||||
$internalToken = auth_header_value('X-Internal-Auth');
|
||||
if($internalToken){
|
||||
$internal = internal_auth_verify($internalToken);
|
||||
if($internal){
|
||||
$context['mode'] = 'internal';
|
||||
$context['user'] = [
|
||||
'id' => isset($internal['sub']) ? intval($internal['sub']) : 0,
|
||||
'email' => isset($internal['email']) ? $internal['email'] : 'internal',
|
||||
'name' => isset($internal['name']) ? $internal['name'] : 'Internal',
|
||||
'organization' => isset($internal['organization']) ? $internal['organization'] : 'Internal',
|
||||
'role' => isset($internal['role']) ? $internal['role'] : 'admin',
|
||||
'status' => 'active'
|
||||
];
|
||||
return $context;
|
||||
}
|
||||
}
|
||||
|
||||
$bearer = auth_header_value('Authorization');
|
||||
if($bearer && stripos($bearer, 'Bearer ') === 0){
|
||||
$payload = jwt_verify(trim(substr($bearer, 7)));
|
||||
if($payload && isset($payload['sub'])){
|
||||
$uid = intval($payload['sub']);
|
||||
if($uid > 0 && $conn){
|
||||
$st = $conn->prepare('SELECT id, email, name, organization, role, status FROM users WHERE id = ? LIMIT 1');
|
||||
if($st){
|
||||
$st->bind_param('i', $uid);
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$row = $r ? $r->fetch_assoc() : null;
|
||||
$st->close();
|
||||
if($row){
|
||||
$context['mode'] = 'jwt';
|
||||
$context['user'] = $row;
|
||||
return $context;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list($basicUser, $basicPass) = auth_basic_credentials();
|
||||
if($basicUser !== null){
|
||||
if($basicUser === BASIC_API_USER && hash_equals(BASIC_API_PASS, (string)$basicPass)){
|
||||
$context['mode'] = 'basic';
|
||||
$context['user'] = ['id' => 0, 'email' => 'api', 'name' => 'API', 'organization' => 'API', 'role' => 'admin', 'status' => 'active'];
|
||||
return $context;
|
||||
}
|
||||
if($conn){
|
||||
$st = $conn->prepare('SELECT id, email, name, organization, role, status, password_hash FROM users WHERE email = ? LIMIT 1');
|
||||
if($st){
|
||||
$st->bind_param('s', $basicUser);
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$row = $r ? $r->fetch_assoc() : null;
|
||||
$st->close();
|
||||
if($row && password_verify((string)$basicPass, $row['password_hash'])){
|
||||
unset($row['password_hash']);
|
||||
$context['mode'] = 'basic';
|
||||
$context['user'] = $row;
|
||||
return $context;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($_SESSION['jwt'])){
|
||||
$payload = jwt_verify($_SESSION['jwt']);
|
||||
if($payload && isset($payload['sub']) && $conn){
|
||||
$uid = intval($payload['sub']);
|
||||
$st = $conn->prepare('SELECT id, email, name, organization, role, status FROM users WHERE id = ? LIMIT 1');
|
||||
if($st){
|
||||
$st->bind_param('i', $uid);
|
||||
$st->execute();
|
||||
$r = $st->get_result();
|
||||
$row = $r ? $r->fetch_assoc() : null;
|
||||
$st->close();
|
||||
if($row){
|
||||
$context['mode'] = 'jwt';
|
||||
$context['user'] = $row;
|
||||
return $context;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
function get_current_user_info(){
|
||||
$context = get_auth_context();
|
||||
return $context['user'];
|
||||
}
|
||||
|
||||
function require_auth($allowedModes = ['jwt','basic','internal']){
|
||||
$context = get_auth_context();
|
||||
$user = $context['user'];
|
||||
if(!$user){ auth_json_response(401, ['success'=>false,'error'=>'unauthenticated']); }
|
||||
if(isset($user['status']) && $user['status'] !== 'active'){
|
||||
auth_json_response(403, ['success'=>false,'error'=>'account_not_active']);
|
||||
}
|
||||
if($allowedModes && $context['mode'] && !in_array($context['mode'], (array)$allowedModes, true)){
|
||||
auth_json_response(403, ['success'=>false,'error'=>'forbidden']);
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
function require_login(){
|
||||
return require_auth(['jwt','basic','internal']);
|
||||
}
|
||||
|
||||
function require_bearer_login(){
|
||||
$context = get_auth_context();
|
||||
if(!$context['user'] || $context['mode'] !== 'jwt'){
|
||||
auth_json_response(401, ['success'=>false,'error'=>'bearer_required']);
|
||||
}
|
||||
if(isset($context['user']['status']) && $context['user']['status'] !== 'active'){
|
||||
auth_json_response(403, ['success'=>false,'error'=>'account_not_active']);
|
||||
}
|
||||
return $context['user'];
|
||||
}
|
||||
|
||||
function require_role($roles){
|
||||
$user = require_login();
|
||||
// Allow admin to have universal access regardless of requested roles
|
||||
if(isset($user['role']) && $user['role'] === 'admin'){
|
||||
return $user;
|
||||
}
|
||||
if(!in_array($user['role'], (array)$roles, true)){
|
||||
auth_json_response(403, ['success'=>false,'error'=>'forbidden']);
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
function auth_current_mode(){
|
||||
$context = get_auth_context();
|
||||
return $context['mode'];
|
||||
}
|
||||
|
||||
function auth_issue_user_token($userId, $role, $extra = [], $ttlSeconds = 86400){
|
||||
return jwt_issue(array_merge(['sub' => intval($userId), 'role' => $role], $extra), $ttlSeconds);
|
||||
}
|
||||
|
||||
function auth_set_session_jwt($token){
|
||||
start_session_if_needed();
|
||||
$_SESSION['jwt'] = $token;
|
||||
}
|
||||
|
||||
function auth_clear_session(){
|
||||
start_session_if_needed();
|
||||
foreach(['jwt','user_id'] as $key){ if(isset($_SESSION[$key])) unset($_SESSION[$key]); }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
// Load environment from project root .env if present, then read API_KEY
|
||||
function load_dotenv($path){
|
||||
if(!file_exists($path)) return;
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
|
||||
foreach($lines as $line){
|
||||
if(trim($line)==='' || strpos(trim($line),'#')===0) continue;
|
||||
if(strpos($line,'=')===false) continue;
|
||||
list($k,$v) = explode('=', $line, 2);
|
||||
$k = trim($k); $v = trim($v);
|
||||
// strip optional quotes
|
||||
if((substr($v,0,1)==='"' && substr($v,-1)==='"') || (substr($v,0,1)==="'" && substr($v,-1)==="'")){
|
||||
$v = substr($v,1,-1);
|
||||
}
|
||||
putenv("$k=$v");
|
||||
$_ENV[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
$envPath = realpath(__DIR__ . '/../../.env');
|
||||
if($envPath){ load_dotenv($envPath); }
|
||||
|
||||
$apiKey = getenv('API_KEY');
|
||||
if(!$apiKey) {
|
||||
// fallback (development) - but prefer .env or system env in production
|
||||
$apiKey = '8f3b7e6a9c4d2f1a6b8c9d0e4f7a1b2c';
|
||||
}
|
||||
define('API_KEY', $apiKey);
|
||||
|
||||
$jwtSecret = getenv('JWT_SECRET');
|
||||
if(!$jwtSecret) {
|
||||
$jwtSecret = hash('sha256', $apiKey . ':jwt');
|
||||
}
|
||||
define('JWT_SECRET', $jwtSecret);
|
||||
|
||||
$internalKey = getenv('INTERNAL_AUTH_KEY');
|
||||
if(!$internalKey) {
|
||||
$internalKey = hash('sha256', $apiKey . ':internal');
|
||||
}
|
||||
define('INTERNAL_AUTH_KEY', $internalKey);
|
||||
|
||||
$basicApiUser = getenv('BASIC_API_USER');
|
||||
if(!$basicApiUser) { $basicApiUser = 'webgis-api'; }
|
||||
define('BASIC_API_USER', $basicApiUser);
|
||||
|
||||
$basicApiPass = getenv('BASIC_API_PASS');
|
||||
if(!$basicApiPass) { $basicApiPass = hash('sha256', $apiKey . ':basic'); }
|
||||
define('BASIC_API_PASS', $basicApiPass);
|
||||
?>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
$dbHost = getenv('DB_HOST') ?: 'localhost';
|
||||
$dbPort = (int)(getenv('DB_PORT') ?: 3306);
|
||||
$dbUser = getenv('DB_USER') ?: 'root';
|
||||
$dbPass = getenv('DB_PASS') ?: 'ilham';
|
||||
$dbName = getenv('DB_NAME') ?: 'webgis';
|
||||
|
||||
$conn = new mysqli($dbHost, $dbUser, $dbPass, $dbName, $dbPort);
|
||||
|
||||
if ($conn->connect_error) {
|
||||
die("Koneksi gagal: " . $conn->connect_error);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
if(!$data){
|
||||
http_response_code(400);
|
||||
echo json_encode(["success"=>false,"error"=>"invalid_json"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$nama = isset($data['nama']) ? $conn->real_escape_string($data['nama']) : '';
|
||||
$status = isset($data['status']) ? $conn->real_escape_string($data['status']) : '';
|
||||
$panjang = isset($data['panjang']) ? floatval($data['panjang']) : 0;
|
||||
$geom = isset($data['geom']) ? $conn->real_escape_string(json_encode($data['geom'])) : '';
|
||||
|
||||
$sql = "INSERT INTO jalan (nama,status,panjang,geom) VALUES ('$nama','$status','$panjang','$geom')";
|
||||
|
||||
if($conn->query($sql) === TRUE){
|
||||
$insert_id = $conn->insert_id;
|
||||
echo json_encode(["success"=>true,"id"=>$insert_id]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(["success"=>false,"error"=>$conn->error]);
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
$id = rand(1000,9999);
|
||||
$nama = isset($data['nama']) ? $conn->real_escape_string($data['nama']) : '';
|
||||
$no_telp = isset($data['no_telp']) ? $conn->real_escape_string($data['no_telp']) : '';
|
||||
$buka = isset($data['buka_24_jam']) ? intval($data['buka_24_jam']) : 0;
|
||||
$alamat = isset($data['alamat']) ? $conn->real_escape_string($data['alamat']) : '';
|
||||
$jenis = isset($data['jenis']) ? $conn->real_escape_string($data['jenis']) : '';
|
||||
$lat = isset($data['latitude']) ? $data['latitude'] : '';
|
||||
$lng = isset($data['longitude']) ? $data['longitude'] : '';
|
||||
|
||||
// Insert specifying columns (ensure your `lokasi` table has `jenis` and `alamat` columns)
|
||||
$sql = "INSERT INTO lokasi (id, nama, jenis, no_telp, buka_24_jam, alamat, latitude, longitude) VALUES ('$id', '$nama', '$jenis', '$no_telp', '$buka', '$alamat', '$lat', '$lng')";
|
||||
$res = $conn->query($sql);
|
||||
|
||||
if($res){
|
||||
echo json_encode(["success"=>true, "id"=>$id]);
|
||||
} else {
|
||||
echo json_encode(["success"=>false, "error"=> $conn->error]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
if(!$data){
|
||||
http_response_code(400);
|
||||
echo json_encode(["success"=>false,"error"=>"invalid_json"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$nama = isset($data['nama']) ? $conn->real_escape_string($data['nama']) : '';
|
||||
$status = isset($data['status']) ? $conn->real_escape_string($data['status']) : '';
|
||||
$luas = isset($data['luas']) ? floatval($data['luas']) : 0;
|
||||
$geom = isset($data['geom']) ? $conn->real_escape_string(json_encode($data['geom'])) : '';
|
||||
|
||||
$sql = "INSERT INTO tanah (nama,status,luas,geom) VALUES ('$nama','$status','$luas','$geom')";
|
||||
|
||||
if($conn->query($sql) === TRUE){
|
||||
$insert_id = $conn->insert_id;
|
||||
echo json_encode(["success"=>true,"id"=>$insert_id]);
|
||||
// log
|
||||
@file_put_contents('/tmp/webgis_debug.log', date('c') . " tambah_tanah OK id=".$insert_id." payload=".json_encode($data)."\n", FILE_APPEND);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(["success"=>false,"error"=>$conn->error]);
|
||||
@file_put_contents('/tmp/webgis_debug.log', date('c') . " tambah_tanah ERR=".$conn->error." sql=".$sql." payload=".json_encode($data)."\n", FILE_APPEND);
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
$id = $data['id'];
|
||||
$sets = array();
|
||||
if(isset($data['nama'])) $sets[] = "nama='".$data['nama']."'";
|
||||
if(isset($data['status'])) $sets[] = "status='".$data['status']."'";
|
||||
if(isset($data['panjang'])) $sets[] = "panjang='".$data['panjang']."'";
|
||||
if(isset($data['geom'])){
|
||||
$geom = json_encode($data['geom']);
|
||||
$sets[] = "geom='".$geom."'";
|
||||
}
|
||||
|
||||
if(count($sets) > 0){
|
||||
$sql = "UPDATE jalan SET " . implode(",", $sets) . " WHERE id='".$id."'";
|
||||
$conn->query($sql);
|
||||
}
|
||||
|
||||
echo json_encode(["msg"=>"ok"]);
|
||||
?>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
$id = isset($data['id']) ? $conn->real_escape_string($data['id']) : '';
|
||||
$nama = isset($data['nama']) ? $conn->real_escape_string($data['nama']) : '';
|
||||
$no_telp = isset($data['no_telp']) ? $conn->real_escape_string($data['no_telp']) : '';
|
||||
$buka = isset($data['buka_24_jam']) ? intval($data['buka_24_jam']) : 0;
|
||||
$alamat = isset($data['alamat']) ? $conn->real_escape_string($data['alamat']) : '';
|
||||
$jenis = isset($data['jenis']) ? $conn->real_escape_string($data['jenis']) : '';
|
||||
|
||||
$sql = "UPDATE lokasi SET nama='$nama', no_telp='$no_telp', buka_24_jam='$buka', alamat='$alamat', jenis='$jenis' WHERE id='$id'";
|
||||
$res = $conn->query($sql);
|
||||
|
||||
if($res){
|
||||
echo json_encode(["success"=>true]);
|
||||
} else {
|
||||
echo json_encode(["success"=>false, "error"=>$conn->error]);
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
$id = $data['id'];
|
||||
$sets = array();
|
||||
if(isset($data['nama'])) $sets[] = "nama='".$data['nama']."'";
|
||||
if(isset($data['status'])) $sets[] = "status='".$data['status']."'";
|
||||
if(isset($data['luas'])) $sets[] = "luas='".$data['luas']."'";
|
||||
if(isset($data['geom'])){
|
||||
$geom = json_encode($data['geom']);
|
||||
$sets[] = "geom='".$geom."'";
|
||||
}
|
||||
|
||||
if(count($sets) > 0){
|
||||
$sql = "UPDATE tanah SET " . implode(",", $sets) . " WHERE id='".$id."'";
|
||||
$res = $conn->query($sql);
|
||||
if($res){
|
||||
@file_put_contents('/tmp/webgis_debug.log', date('c') . " update_tanah OK id=".$id." sql=".$sql." payload=".json_encode($data)."\n", FILE_APPEND);
|
||||
} else {
|
||||
@file_put_contents('/tmp/webgis_debug.log', date('c') . " update_tanah ERR=".$conn->error." sql=".$sql." payload=".json_encode($data)."\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(["msg"=>"ok"]);
|
||||
?>
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
-- MySQL dump 10.13 Distrib 8.0.45, for Linux (x86_64)
|
||||
--
|
||||
-- Host: localhost Database: webgis
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 8.0.45-0ubuntu0.22.04.1
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!50503 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `jalan`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `jalan`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `jalan` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`nama` varchar(100) DEFAULT NULL,
|
||||
`status` varchar(50) DEFAULT NULL,
|
||||
`panjang` double DEFAULT NULL,
|
||||
`geom` text,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `jalan`
|
||||
--
|
||||
|
||||
LOCK TABLES `jalan` WRITE;
|
||||
/*!40000 ALTER TABLE `jalan` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `jalan` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `lokasi`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `lokasi`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `lokasi` (
|
||||
`id` varchar(8) NOT NULL,
|
||||
`nama` varchar(50) NOT NULL,
|
||||
`no_telp` varchar(20) DEFAULT NULL,
|
||||
`buka_24_jam` tinyint(1) DEFAULT NULL,
|
||||
`latitude` decimal(10,6) DEFAULT NULL,
|
||||
`longitude` decimal(10,6) DEFAULT NULL,
|
||||
`alamat` text,
|
||||
`jenis` varchar(50) NOT NULL DEFAULT 'spbu',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_lokasi_alamat` ((left(`alamat`,255)))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `lokasi`
|
||||
--
|
||||
|
||||
LOCK TABLES `lokasi` WRITE;
|
||||
/*!40000 ALTER TABLE `lokasi` DISABLE KEYS */;
|
||||
INSERT INTO `lokasi` VALUES ('5092','Rumah','',0,-0.040882,109.335197,'Daeng Abdul Hadi, Akcaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 78117, Indonesia','rumah'),('7853','Masjid Mujahiddin','',1,-0.041462,109.336345,'Mujahiddin, Jalan Mujahidin, Akcaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 78121, Indonesia','masjid'),('8476','SPBU OSO MT. Haryono','',0,-0.044863,109.336726,'SPBU OSO MT. Haryono, Jalan M.T. Haryono, Akcaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 78121, Indonesia','spbu');
|
||||
/*!40000 ALTER TABLE `lokasi` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `tanah`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `tanah`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `tanah` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`nama` varchar(100) DEFAULT NULL,
|
||||
`status` varchar(50) DEFAULT NULL,
|
||||
`luas` double DEFAULT NULL,
|
||||
`geom` text,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `tanah`
|
||||
--
|
||||
|
||||
LOCK TABLES `tanah` WRITE;
|
||||
/*!40000 ALTER TABLE `tanah` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `tanah` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2026-04-23 9:46:58
|
||||
Reference in New Issue
Block a user