Skip to content

Commit e033c8a

Browse files
authored
Merge pull request #881 from OpenSID/rilis-v2601.0.0
Rilis v2601.0.0
2 parents 82fd0c0 + 54fe257 commit e033c8a

File tree

75 files changed

+2857
-37824
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+2857
-37824
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,8 @@ OTP_RESEND_DECAY_SECONDS=30
102102
# Telegram Bot Configuration
103103
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
104104
TELEGRAM_BOT_NAME=@your_bot_username_here
105+
106+
# Global Rate Limiter Configuration
107+
RATE_LIMITER_ENABLED=false
108+
RATE_LIMITER_MAX_ATTEMPTS=60
109+
RATE_LIMITER_DECAY_MINUTES=1

app/Helpers/general.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
*/
3333
function openkab_versi()
3434
{
35-
return 'v2512.0.1';
35+
return 'v2601.0.0';
3636
}
3737
}
3838

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Master;
4+
5+
use App\Http\Controllers\Controller;
6+
use Illuminate\View\View;
7+
8+
class ArtikelKabupatenController extends Controller
9+
{
10+
protected $permission = 'master-data-artikel';
11+
12+
/**
13+
* Display a listing of the resource.
14+
*
15+
* @return \Illuminate\Http\Response
16+
*/
17+
public function index()
18+
{
19+
$listPermission = $this->generateListPermission();
20+
$clearCache = request('clear_cache', false);
21+
if ($clearCache) {
22+
(new \App\Services\ArtikelService)->clearCache('artikel', ['filter[id]' => $clearCache]);
23+
}
24+
25+
return view('master.artikel.index')->with($listPermission);
26+
}
27+
28+
/**
29+
* Show the form for creating a new resource.
30+
*
31+
* @return \Illuminate\Http\Response
32+
*/
33+
public function create(): View
34+
{
35+
return view('master.artikel.create');
36+
}
37+
38+
/**
39+
* Show the form for editing the specified resource.
40+
*
41+
* @param int $id
42+
*
43+
* @return \Illuminate\Http\Response
44+
*/
45+
public function edit($id): View
46+
{
47+
return view('master.artikel.edit', compact('id'));
48+
}
49+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Master;
4+
5+
use App\Http\Controllers\AppBaseController;
6+
use App\Traits\UploadedFile;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Support\Facades\Storage;
9+
10+
class ArtikelUploadController extends AppBaseController
11+
{
12+
use UploadedFile;
13+
14+
public function __construct()
15+
{
16+
$this->pathFolder = 'uploads/artikel';
17+
}
18+
19+
/**
20+
* Upload gambar artikel
21+
*/
22+
public function uploadGambar(Request $request)
23+
{
24+
try {
25+
$request->validate([
26+
'file' => 'required|image|mimes:jpg,jpeg,png,gif|max:2048',
27+
]);
28+
29+
if ($request->file('file')) {
30+
$path = $this->uploadFile($request, 'file');
31+
$url = Storage::url($path);
32+
33+
return response()->json([
34+
'success' => true,
35+
'url' => $url,
36+
'path' => $path,
37+
], 200);
38+
}
39+
40+
return response()->json([
41+
'success' => false,
42+
'message' => 'File tidak ditemukan',
43+
], 400);
44+
} catch (\Exception $e) {
45+
return response()->json([
46+
'success' => false,
47+
'message' => 'Upload gagal: ' . $e->getMessage(),
48+
], 500);
49+
}
50+
}
51+
}

app/Http/Controllers/StatistikController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public function bantuan()
4444
'detailLink' => url('penduduk'),
4545
'judul' => 'Bantuan',
4646
'default_kategori' => 'penduduk',
47+
'filter_tahun' => true
4748
]);
4849
}
4950

app/Http/Kernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Kernel extends HttpKernel
2121
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
2222
Middleware\TrimStrings::class,
2323
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
24+
Middleware\GlobalRateLimiter::class,
2425
];
2526

2627
/**
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Cache\RateLimiter;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Support\Facades\App;
9+
use Symfony\Component\HttpFoundation\Response;
10+
11+
class GlobalRateLimiter
12+
{
13+
/**
14+
* The rate limiter instance.
15+
*
16+
* @var \Illuminate\Cache\RateLimiter
17+
*/
18+
protected $limiter;
19+
20+
/**
21+
* Create a new rate limiter middleware instance.
22+
*
23+
* @param \Illuminate\Cache\RateLimiter $limiter
24+
* @return void
25+
*/
26+
public function __construct(RateLimiter $limiter)
27+
{
28+
$this->limiter = $limiter;
29+
}
30+
31+
/**
32+
* Handle an incoming request.
33+
*
34+
* @param \Illuminate\Http\Request $request
35+
* @param \Closure $next
36+
* @return \Symfony\Component\HttpFoundation\Response
37+
*/
38+
public function handle(Request $request, Closure $next): Response
39+
{
40+
// Check if global rate limiter is enabled
41+
if (!config('rate-limiter.enabled', false)) {
42+
return $next($request);
43+
}
44+
45+
// Check if current IP should be excluded
46+
if ($this->shouldExcludeIp($request)) {
47+
return $next($request);
48+
}
49+
50+
// Check if current path should be excluded
51+
if ($this->shouldExcludePath($request)) {
52+
return $next($request);
53+
}
54+
55+
// Get configuration from .env or use defaults
56+
$maxAttempts = config('rate-limiter.max_attempts', 60);
57+
$decayMinutes = config('rate-limiter.decay_minutes', 1);
58+
59+
// Generate unique key for this request based on IP
60+
$key = $this->resolveRequestSignature($request);
61+
62+
// Check if the request limit has been exceeded
63+
if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
64+
return $this->buildResponse($key, $maxAttempts);
65+
}
66+
67+
// Add hit to the limiter
68+
$this->limiter->hit($key, $decayMinutes * 60);
69+
70+
$response = $next($request);
71+
72+
// Add headers to the response
73+
$response->headers->set('X-RateLimit-Limit', $maxAttempts);
74+
$response->headers->set('X-RateLimit-Remaining', max(0, $maxAttempts - $this->limiter->attempts($key)));
75+
$response->headers->set('X-RateLimit-Reset', $this->limiter->availableIn($key));
76+
77+
return $response;
78+
}
79+
80+
/**
81+
* Resolve request signature.
82+
*
83+
* @param \Illuminate\Http\Request $request
84+
* @return string
85+
*/
86+
protected function resolveRequestSignature(Request $request): string
87+
{
88+
// Use IP address as the signature for global rate limiting
89+
return sha1(
90+
'global-rate-limit:' . $request->ip()
91+
);
92+
}
93+
94+
/**
95+
* Create a 'too many attempts' response.
96+
*
97+
* @param string $key
98+
* @param int $maxAttempts
99+
* @return \Symfony\Component\HttpFoundation\Response
100+
*/
101+
protected function buildResponse(string $key, int $maxAttempts): Response
102+
{
103+
$seconds = $this->limiter->availableIn($key);
104+
$request = request();
105+
106+
if (App::runningInConsole() || $request->expectsJson()) {
107+
return response()->json([
108+
'message' => 'Too many requests. Please try again later.',
109+
'status' => 'error',
110+
'code' => 429,
111+
'retry_after' => $seconds,
112+
], 429);
113+
}
114+
115+
return response('Too Many Attempts.', 429, [
116+
'Retry-After' => $seconds,
117+
'X-RateLimit-Limit' => $maxAttempts,
118+
'X-RateLimit-Remaining' => 0,
119+
'X-RateLimit-Reset' => $seconds,
120+
]);
121+
}
122+
123+
/**
124+
* Determine if the request IP should be excluded from rate limiting.
125+
*
126+
* @param \Illuminate\Http\Request $request
127+
* @return bool
128+
*/
129+
protected function shouldExcludeIp(Request $request): bool
130+
{
131+
$excludeIps = config('rate-limiter.exclude_ips', []);
132+
133+
return in_array($request->ip(), $excludeIps);
134+
}
135+
136+
/**
137+
* Determine if the request path should be excluded from rate limiting.
138+
*
139+
* @param \Illuminate\Http\Request $request
140+
* @return bool
141+
*/
142+
protected function shouldExcludePath(Request $request): bool
143+
{
144+
$excludePaths = config('rate-limiter.exclude_paths', []);
145+
$requestPath = $request->path();
146+
147+
foreach ($excludePaths as $path) {
148+
if ($this->pathMatches($path, $requestPath)) {
149+
return true;
150+
}
151+
}
152+
153+
return false;
154+
}
155+
156+
/**
157+
* Check if the path matches the pattern.
158+
*
159+
* @param string $pattern
160+
* @param string $path
161+
* @return bool
162+
*/
163+
protected function pathMatches(string $pattern, string $path): bool
164+
{
165+
// Convert wildcard pattern to regex
166+
$pattern = preg_quote($pattern, '#');
167+
$pattern = str_replace('\*', '.*', $pattern);
168+
169+
return preg_match("#^{$pattern}$#", $path);
170+
}
171+
}

app/Services/ArtikelService.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use Illuminate\Support\Facades\Cache;
6+
7+
class ArtikelService extends BaseApiService
8+
{
9+
protected int $cacheTtl = 3600; // TTL dalam detik (1 jam)
10+
11+
public function artikel(array $filters = [])
12+
{
13+
$cacheKey = $this->buildCacheKey('artikel', $filters);
14+
15+
// Ambil dari cache dulu
16+
return Cache::remember($cacheKey, $this->cacheTtl, function () use ($filters) {
17+
$data = $this->apiRequest('/api/v1/artikel', $filters);
18+
if (! $data) {
19+
return collect([]);
20+
}
21+
22+
return collect($data)->map(fn ($item) => (object) $item['attributes']);
23+
});
24+
}
25+
26+
public function artikelById(int $id)
27+
{
28+
$cacheKey = "artikel_$id";
29+
30+
return Cache::remember($cacheKey, $this->cacheTtl, function () use ($id) {
31+
$data = $this->apiRequest('/api/v1/artikel/tampil', [
32+
'id' => $id,
33+
]);
34+
35+
if (is_array($data) && isset($data['data'])) {
36+
return (object) $data['data'];
37+
}
38+
39+
return null;
40+
});
41+
}
42+
43+
public function clearCache(string $prefix = 'artikel', array $filters = [])
44+
{
45+
$cacheKey = $this->buildCacheKey($prefix, $filters);
46+
Cache::forget($cacheKey);
47+
}
48+
}

catatan_rilis.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
Di rilis ini, versi 2512.0.1 berisi penambahan dan perbaikan yang diminta pengguna.
1+
Di rilis ini, versi 2601.0.0 berisi penambahan dan perbaikan yang diminta pengguna.
22

33
#### Penambahan Fitur
44

5+
1. [#872](https://github.com/OpenSID/OpenKab/issues/872) Penambahan modul artikel OpenSID.
56

67
#### Perbaikan BUG
78

89
1. [#873](https://github.com/OpenSID/OpenKab/issues/873) Perbaikan menu laporan data presisi yang hilang.
10+
2. [#879](https://github.com/OpenSID/OpenKab/issues/879) Perbaikan filter tahun pada statistik kependudukan.
911

1012
#### Perubahan Teknis
1113

14+
1. [#869](https://github.com/OpenSID/OpenKab/issues/869) Upgrade versi moment pada chart.js serta perbaikan halaman website presisi untuk kependudukan dan RTM.
15+
2. [#876](https://github.com/OpenSID/OpenKab/issues/876) Ganti highchart dengan chartjs agar menggunakan satu library saja.
16+
3. [#868](https://github.com/OpenSID/OpenKab/issues/868) Penerapan rate limiting pada OpenKab untuk membantu mencegah serangan DDOS.

0 commit comments

Comments
 (0)